diff --git a/.buildkite/Dockerfile b/.buildkite/Dockerfile deleted file mode 100644 index d9d13f16e6..0000000000 --- a/.buildkite/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# Heavily based on https://github.com/StefanScherer/dockerfiles-windows/ images. -# Combines the node windowsservercore image with the Bazel Prerequisites (https://docs.bazel.build/versions/master/install-windows.html). -# msys install taken from https://github.com/StefanScherer/dockerfiles-windows/issues/30 -# VS redist install taken from https://github.com/StefanScherer/dockerfiles-windows/blob/master/apache/Dockerfile -# The nanoserver image won't work because MSYS2 does not run in it https://github.com/Alexpux/MSYS2-packages/issues/1493 - -# Before building this image, you must locally build node-windows:10.13.0-windowsservercore-1803. -# Clone https://github.com/StefanScherer/dockerfiles-windows/commit/4ce7101a766b9b880ac262479dd9126b64d656cf and build using -# docker build -t node-windows:10.13.0-windowsservercore-1803 --build-arg core=microsoft/windowsservercore:1803 --build-arg target=microsoft/windowsservercore:1803 . -FROM node-windows:10.13.0-windowsservercore-1803 - -SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] - -# Install 7zip to extract msys2 -RUN Invoke-WebRequest -UseBasicParsing 'https://www.7-zip.org/a/7z1805-x64.exe' -OutFile 7z.exe -# For some reason the last letter in the destination directory is lost. So '/D=C:\\7zip0' will extract to '/D=C:\\7zip'. -RUN Start-Process -FilePath 'C:\\7z.exe' -ArgumentList '/S', '/D=C:\\7zip0' -NoNewWindow -Wait - -# Extract msys2 -RUN Invoke-WebRequest -UseBasicParsing 'http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20180531.tar.xz' -OutFile msys2.tar.xz -RUN Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'e', 'msys2.tar.xz' -Wait -RUN Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'x', 'msys2.tar', '-oC:\\' -Wait -RUN Remove-Item msys2.tar.xz -RUN Remove-Item msys2.tar -RUN Remove-Item 7z.exe -RUN Remove-Item -Recurse 7zip - -# Add MSYS2 to PATH, and set BAZEL_SH -RUN [Environment]::SetEnvironmentVariable('Path', $env:Path + ';C:\msys64\usr\bin', [System.EnvironmentVariableTarget]::Machine) -RUN [Environment]::SetEnvironmentVariable('BAZEL_SH', 'C:\msys64\usr\bin\bash.exe', [System.EnvironmentVariableTarget]::Machine) - -# Install Microsoft Visual C++ Redistributable for Visual Studio 2015 -RUN Invoke-WebRequest -UseBasicParsing 'https://download.microsoft.com/download/9/3/F/93FCF1E7-E6A4-478B-96E7-D4B285925B00/vc_redist.x64.exe' -OutFile vc_redist.x64.exe -RUN Start-Process 'c:\\vc_redist.x64.exe' -ArgumentList '/Install', '/Passive', '/NoRestart' -NoNewWindow -Wait -RUN Remove-Item vc_redist.x64.exe - -# Add a fix for https://github.com/docker/for-win/issues/2920 as entry point to the container. -SHELL ["cmd", "/c"] -COPY "fix-msys64.cmd" "C:\\fix-msys64.cmd" -ENTRYPOINT cmd /C C:\\fix-msys64.cmd && cmd /c - -CMD ["cmd.exe"] diff --git a/.buildkite/README.md b/.buildkite/README.md deleted file mode 100644 index b7e25cd93b..0000000000 --- a/.buildkite/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# BuildKite configuration - -This folder contains configuration for the [BuildKite](https://buildkite.com) based CI checks for -this repository. - -BuildKite is a CI provider that provides build coordination and reports while we provide the -infrastructure. - -CI runs are triggered by new PRs and will show up on the GitHub checks interface, along with the -other current CI solutions. - -Currently it is only used for tests on Windows platforms. - - -## The build pipeline - -BuildKite uses a pipeline for each repository. The `pipeline.yml` file defines pipeline -[build steps](https://buildkite.com/docs/pipelines/defining-steps) for this repository. - -Run results can be seen in the GitHub checks interface and in the -[pipeline dashboard](https://buildkite.com/angular/angular). - -Although most configuration is done via `pipeline.yml`, some options are only available -in the online [pipeline settings](https://buildkite.com/angular/angular/settings). - - -## Infrastructure - -BuildKite does not provide the host machines where the builds runs, providing instead the -[BuildKite Agent](https://buildkite.com/docs/agent/v3) that should be run our own infrastructure. - - -### Agents - -This agent polls the BuildKite API for builds, runs them, and reports back the results. -Agents are the unit of concurrency: each agent can run one build at any given time. -Adding agents allows more builds to be ran at the same time. - -Individual agents can have tags, and pipeline steps can target only agents with certain tags via the -`agents` field in `pipeline.yml`. -For example: agents on Windows machines are tagged as `windows`, and the Windows specific build -steps list `windows: true` in their `agents` field. - -You can see the current agent pool, along with their tags, in the -[agents list](https://buildkite.com/organizations/angular/agents). - - -### Our host machines - -We use [Google Cloud](https://cloud.google.com/) as our cloud provider, under the -[Angular project](https://console.cloud.google.com/home/dashboard?project=internal-200822). -To access this project you need need to be logged in with a Google account that's a member of -team@angular.io. -For googlers this may be your google.com account, for others it is an angular.io account. - -In this project we have a number of Windows VMs running, each of them with several agents. -The `provision-windows-buildkite.ps1` file contains instructions on how to create new host VMs that -are fully configured to run the BuildKite agents as services. - -Our pipeline uses [docker-buildkite-plugin](https://github.com/buildkite-plugins/docker-buildkite-plugin) -to run build steps inside docker containers. -This way we achieve isolation and hermeticity. - -The `Dockerfile` file describes a custom Docker image that includes NodeJs, Yarn, and the Bazel -pre-requisites on Windows. - -To upload a new version of the docker image, follow any build instructions in `Dockerfile` and then -run `docker build -t angular/node-bazel-windows:NEW_VERSION`, followed by -`docker push angular/node-bazel-windows:NEW_VERSION`. -After being pushed it should be available online, and you can use the new version in `pipeline.yml`. - - -## Caretaker - -BuildKite status can be found at https://www.buildkitestatus.com/. - -Issues related to the BuildKite setup should be escalated to the Tools Team via the current -caretaker, followed by Alex Eagle and Filipe Silva. - -Support requests should be submitted via email to support@buildkite.com and cc Igor, Misko, Alex, -Jeremy and Manu - - -## Rollout strategy - -At the moment our BuildKite CI uses 1 host VM running 4 agents, thus being capable of 4 concurrent -builds. -The only test running is `bazel test //tools/ts-api-guardian:all`, and the PR check is not -mandatory. - -In the future we should add cache support to speed up the initial `yarn` install, and also Bazel -remote caching to speed up Bazel builds. - -After the current setup is verified as stable and reliable the GitHub PR check can become mandatory. - -The tests ran should also be expanded to cover most, if not all, of the Bazel tests. diff --git a/.buildkite/fix-msys64.cmd b/.buildkite/fix-msys64.cmd deleted file mode 100644 index 4722e6edd6..0000000000 --- a/.buildkite/fix-msys64.cmd +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -REM Fix for https://github.com/docker/for-win/issues/2920 -REM echo "Fixing msys64 folder..." -REM Touch all .dll files inside C:\msys64\ -forfiles /p C:\msys64\ /s /m *.dll /c "cmd /c Copy /B @path+,, >NUL" -REM echo "Fixed msys64 folder." diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml deleted file mode 100644 index 58dd597bc4..0000000000 --- a/.buildkite/pipeline.yml +++ /dev/null @@ -1,10 +0,0 @@ -steps: - - label: windows-test - commands: - - "yarn install --frozen-lockfile --non-interactive --network-timeout 100000" - - "yarn bazel test //tools/ts-api-guardian:all --noshow_progress" - plugins: - - docker#v2.1.0: - image: "filipesilva/node-bazel-windows:0.0.2" - agents: - windows: true diff --git a/.buildkite/provision-windows-buildkite.ps1 b/.buildkite/provision-windows-buildkite.ps1 deleted file mode 100644 index 7af03b2319..0000000000 --- a/.buildkite/provision-windows-buildkite.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -# PowerShell script to provision a Windows Server with BuildKite -# This script follows https://buildkite.com/docs/agent/v3/windows. - -# Instructions - -# VM creation: -# In Google Cloud Platform, create a Compute Engine instance. -# We recommend machine type n1-standard-16 (16 vCPUs, 60 GB memory). -# Use a recent windows boot disk with container support such as -# "Windows Server version 1803 Datacenter Core for Containers", and add a 128GB SSD disk. -# Give it a name, then click "Create". - -# VM setup: -# In the Compute Engine menu, select "VM Instances". Click on the VM name you chose before. -# Click "Set Windows Password" to choose a username and password. -# Click RDP to open a remote desktop via browser, using the username and password. -# In the Windows command prompt start an elevated powershell by inputing -# "powershell -Command "Start-Process PowerShell -Verb RunAs" followed by Enter. -# Download and execute this script from GitHub, passing the token (mandatory), tags (optional) -# and number of agents (optional) as args: -# ``` -# Invoke-WebRequest -Uri https://raw.githubusercontent.com/angular/angular/master/.buildkite/provision-windows-buildkite.ps1 -OutFile provision.ps1 -# .\provision.ps1 -token "MY_TOKEN" -tags "windows=true,another_tag=true" -agents 4 -# ``` -# The VM should restart and be fully configured. - -# Creating extra VMs -# You can create an image of the current VM by following the instructions below. -# https://cloud.google.com/compute/docs/instances/windows/creating-windows-os-image -# Then create a new VM and choose "Custom images". - - -# Script proper. - -# Get the token and tags from arguments. -param ( - [Parameter(Mandatory=$true)][string]$token, - [string]$tags = "", - [Int]$agents = 1 -) - -# Allow HTTPS -[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" - -# Helper to add to PATH. -# Will take current PATH so avoid running it after anything to modifies only the powershell session path. -function Add-Path ([string]$newPathItem) { - $Env:Path+= ";" + $newPathItem + ";" - [Environment]::SetEnvironmentVariable("Path",$env:Path, [System.EnvironmentVariableTarget]::Machine) -} - -# Install Git for Windows -Write-Host "Installing Git for Windows." -Invoke-WebRequest -Uri https://github.com/git-for-windows/git/releases/download/v2.19.1.windows.1/Git-2.19.1-64-bit.exe -OutFile git.exe -.\git.exe /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh" /DIR="C:\git" -Add-Path "C:\git\bin" -# Sleep for 15s while git is installed. Trying to remove the git.exe before it finishes install causes an error. -Start-Sleep -s 15 -Remove-Item git.exe - -# Download NSSM (https://nssm.cc/) to run the BuildKite agent as a service. -Write-Host "Downloading NSSM." -Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -OutFile nssm.zip -Expand-Archive -Path nssm.zip -DestinationPath C:\nssm -Add-Path "C:\nssm\nssm-2.24-101-g897c7ad\win64" -Remove-Item nssm.zip - -# Run the BuildKite agent install script -Write-Host "Installing BuildKite agent." -$env:buildkiteAgentToken = $token -$env:buildkiteAgentTags = $tags -Set-ExecutionPolicy Bypass -Scope Process -Force -iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/buildkite/agent/master/install.ps1')) - -# Configure the BuildKite agent clone and timestamp behavior -Add-Content C:\buildkite-agent\buildkite-agent.cfg "`ngit-clone-flags=--config core.autocrlf=input --config core.eol=lf --config core.longpaths=true --config core.symlinks=true`n" -Add-Content C:\buildkite-agent\buildkite-agent.cfg "`ntimestamp-lines=true`n" - -# Register the BuildKite agent service using NSSM, so that it persists through restarts and is -# restarted if the process dies. -for ($i=1; $i -le $agents; $i++) -{ - $agentName = "buildkite-agent-$i" - Write-Host "Registering $agentName as a service." - nssm.exe install $agentName "C:\buildkite-agent\bin\buildkite-agent.exe" "start" - nssm.exe set $agentName AppStdout "C:\buildkite-agent\$agentName.log" - nssm.exe set $agentName AppStderr "C:\buildkite-agent\$agentName.log" - nssm.exe status $agentName - nssm.exe start $agentName - nssm.exe status $agentName -} - -# Restart the machine. -Restart-Computer diff --git a/.codefresh/Dockerfile.win-1809 b/.codefresh/Dockerfile.win-1809 index c5169f97eb..eff6e1771a 100644 --- a/.codefresh/Dockerfile.win-1809 +++ b/.codefresh/Dockerfile.win-1809 @@ -1,3 +1,5 @@ +# escape=` + ARG core=mcr.microsoft.com/windows/servercore:1809 ARG target=mcr.microsoft.com/powershell:windowsservercore-1809 @@ -10,57 +12,57 @@ SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPref ENV GPG_VERSION 2.3.4 -RUN Invoke-WebRequest $('https://files.gpg4win.org/gpg4win-vanilla-{0}.exe' -f $env:GPG_VERSION) -OutFile 'gpg4win.exe' -UseBasicParsing ; \ +RUN Invoke-WebRequest $('https://files.gpg4win.org/gpg4win-vanilla-{0}.exe' -f $env:GPG_VERSION) -OutFile 'gpg4win.exe' -UseBasicParsing ; ` Start-Process .\gpg4win.exe -ArgumentList '/S' -NoNewWindow -Wait -RUN @( \ - '94AE36675C464D64BAFA68DD7434390BDBE9B9C5', \ - 'FD3A5288F042B6850C66B31F09FE44734EB7990E', \ - '71DCFD284A79C3B38668286BC97EC7A07EDE3FC1', \ - 'DD8F2338BAE7501E3DD5AC78C273792F7D83545D', \ - 'C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8', \ - 'B9AE9905FFD7803F25714661B63B535A4C206CA9', \ - '77984A986EBC2AA786BC0F66B01FBB92821C587A', \ - '8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600', \ - '4ED778F539E3634C779C87C6D7062848A1AB005C', \ - 'A48C2BEE680E841632CD4E44F07496B3EB3C1762', \ - 'B9E2F5981AA6E0CD28160D9FF13993A75599653C' \ - ) | foreach { \ - gpg --keyserver ha.pool.sks-keyservers.net --recv-keys $_ ; \ +RUN @( ` + '94AE36675C464D64BAFA68DD7434390BDBE9B9C5', ` + 'FD3A5288F042B6850C66B31F09FE44734EB7990E', ` + '71DCFD284A79C3B38668286BC97EC7A07EDE3FC1', ` + 'DD8F2338BAE7501E3DD5AC78C273792F7D83545D', ` + 'C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8', ` + 'B9AE9905FFD7803F25714661B63B535A4C206CA9', ` + '77984A986EBC2AA786BC0F66B01FBB92821C587A', ` + '8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600', ` + '4ED778F539E3634C779C87C6D7062848A1AB005C', ` + 'A48C2BEE680E841632CD4E44F07496B3EB3C1762', ` + 'B9E2F5981AA6E0CD28160D9FF13993A75599653C' ` + ) | foreach { ` + gpg --keyserver ha.pool.sks-keyservers.net --recv-keys $_ ; ` } ENV NODE_VERSION=$node_version -RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; \ +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; ` gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc -RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ - $sum = $(cat SHASUMS256.txt.asc | sls $(' node-v{0}-win-x64.zip' -f $env:NODE_VERSION)) -Split ' ' ; \ - if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $sum[0]) { Write-Error 'SHA256 mismatch' } ; \ - Expand-Archive node.zip -DestinationPath C:\ ; \ +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; ` + $sum = $(cat SHASUMS256.txt.asc | sls $(' node-v{0}-win-x64.zip' -f $env:NODE_VERSION)) -Split ' ' ; ` + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $sum[0]) { Write-Error 'SHA256 mismatch' } ; ` + Expand-Archive node.zip -DestinationPath C:\ ; ` Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' ENV YARN_VERSION=$yarn_version -RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \ - Invoke-WebRequest $('https://yarnpkg.com/downloads/{0}/yarn-{0}.msi' -f $env:YARN_VERSION) -OutFile yarn.msi -UseBasicParsing ; \ - $sig = Get-AuthenticodeSignature yarn.msi ; \ - if ($sig.Status -ne 'Valid') { Write-Error 'Authenticode signature is not valid' } ; \ - Write-Output $sig.SignerCertificate.Thumbprint ; \ - if (@( \ - '7E253367F8A102A91D04829E37F3410F14B68A5F', \ - 'AF764E1EA56C762617BDC757C8B0F3780A0CF5F9' \ - ) -notcontains $sig.SignerCertificate.Thumbprint) { Write-Error 'Unknown signer certificate' } ; \ +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` + Invoke-WebRequest $('https://yarnpkg.com/downloads/{0}/yarn-{0}.msi' -f $env:YARN_VERSION) -OutFile yarn.msi -UseBasicParsing ; ` + $sig = Get-AuthenticodeSignature yarn.msi ; ` + if ($sig.Status -ne 'Valid') { Write-Error 'Authenticode signature is not valid' } ; ` + Write-Output $sig.SignerCertificate.Thumbprint ; ` + if (@( ` + '7E253367F8A102A91D04829E37F3410F14B68A5F', ` + 'AF764E1EA56C762617BDC757C8B0F3780A0CF5F9' ` + ) -notcontains $sig.SignerCertificate.Thumbprint) { Write-Error 'Unknown signer certificate' } ; ` Start-Process msiexec.exe -ArgumentList '/i', 'yarn.msi', '/quiet', '/norestart' -NoNewWindow -Wait ENV GIT_VERSION 2.20.1 ENV GIT_DOWNLOAD_URL https://github.com/git-for-windows/git/releases/download/v${GIT_VERSION}.windows.1/MinGit-${GIT_VERSION}-busybox-64-bit.zip ENV GIT_SHA256 9817ab455d9cbd0b09d8664b4afbe4bbf78d18b556b3541d09238501a749486c -RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \ - Invoke-WebRequest -UseBasicParsing $env:GIT_DOWNLOAD_URL -OutFile git.zip; \ - if ((Get-FileHash git.zip -Algorithm sha256).Hash -ne $env:GIT_SHA256) {exit 1} ; \ - Expand-Archive git.zip -DestinationPath C:\git; \ +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; ` + Invoke-WebRequest -UseBasicParsing $env:GIT_DOWNLOAD_URL -OutFile git.zip; ` + if ((Get-FileHash git.zip -Algorithm sha256).Hash -ne $env:GIT_SHA256) {exit 1} ; ` + Expand-Archive git.zip -DestinationPath C:\git; ` Remove-Item git.zip FROM $target as baseimage @@ -74,30 +76,45 @@ COPY --from=download /git /git ARG SETX=/M RUN setx %SETX% PATH "%PATH%;C:\nodejs;C:\yarn\bin;C:\git\cmd;C:\git\mingw64\bin;C:\git\usr\bin" -CMD [ "node.exe" ] +CMD [ "node.exe" ] FROM baseimage SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] -RUN Invoke-WebRequest -UseBasicParsing 'https://www.7-zip.org/a/7z1805-x64.exe' -OutFile 7z.exe; \ - Start-Process -FilePath 'C:\\7z.exe' -ArgumentList '/S', '/D=C:\\7zip0' -NoNewWindow -Wait; \ - Invoke-WebRequest -UseBasicParsing 'http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20180531.tar.xz' -OutFile msys2.tar.xz; \ - Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'e', 'msys2.tar.xz' -Wait; \ - Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'x', 'msys2.tar', '-oC:\\' -Wait; \ - Remove-Item msys2.tar.xz; \ - Remove-Item msys2.tar; \ - Remove-Item 7z.exe; \ - Remove-Item -Recurse 7zip; \ - [Environment]::SetEnvironmentVariable('Path', $env:Path + ';C:\msys64\usr\bin', [System.EnvironmentVariableTarget]::Machine); \ - [Environment]::SetEnvironmentVariable('BAZEL_SH', 'C:\msys64\usr\bin\bash.exe', [System.EnvironmentVariableTarget]::Machine); \ - Invoke-WebRequest -UseBasicParsing 'https://download.microsoft.com/download/9/3/F/93FCF1E7-E6A4-478B-96E7-D4B285925B00/vc_redist.x64.exe' -OutFile vc_redist.x64.exe; \ - Start-Process 'c:\\vc_redist.x64.exe' -ArgumentList '/Install', '/Passive', '/NoRestart' -NoNewWindow -Wait; \ - Remove-Item vc_redist.x64.exe +RUN Invoke-WebRequest -UseBasicParsing 'https://www.7-zip.org/a/7z1805-x64.exe' -OutFile 7z.exe; ` + Start-Process -FilePath 'C:\\7z.exe' -ArgumentList '/S', '/D=C:\\7zip0' -NoNewWindow -Wait; ` + Invoke-WebRequest -UseBasicParsing 'http://repo.msys2.org/distrib/x86_64/msys2-base-x86_64-20180531.tar.xz' -OutFile msys2.tar.xz; ` + Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'e', 'msys2.tar.xz' -Wait; ` + Start-Process -FilePath 'C:\\7zip\\7z' -ArgumentList 'x', 'msys2.tar', '-oC:\\' -Wait; ` + Remove-Item msys2.tar.xz; ` + Remove-Item msys2.tar; ` + Remove-Item 7z.exe; ` + Remove-Item -Recurse 7zip; ` + [Environment]::SetEnvironmentVariable('Path', $env:Path + ';C:\msys64\usr\bin', [System.EnvironmentVariableTarget]::Machine); ` + [Environment]::SetEnvironmentVariable('BAZEL_SH', 'C:\msys64\usr\bin\bash.exe', [System.EnvironmentVariableTarget]::Machine) -# Add a fix for https://github.com/docker/for-win/issues/2920 as entry point to the container. -SHELL ["cmd", "/c"] -COPY "fix-msys64.cmd" "C:\\fix-msys64.cmd" -ENTRYPOINT cmd /C C:\\fix-msys64.cmd && cmd /c +# Install VS Build Tools +RUN Invoke-WebRequest -UseBasicParsing https://download.visualstudio.microsoft.com/download/pr/df649173-11e9-4af2-8eb7-0eb02ba8958a/cadb5bdac41e55bb8f6a6b7c45273370/vs_buildtools.exe -OutFile vs_BuildTools.exe; ` + # Installer won't detect DOTNET_SKIP_FIRST_TIME_EXPERIENCE if ENV is used, must use setx /M + setx /M DOTNET_SKIP_FIRST_TIME_EXPERIENCE 1; ` + Start-Process vs_BuildTools.exe ` + -ArgumentList ` + '--add', 'Microsoft.VisualStudio.Workload.VCTools', ` + '--add', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', ` + '--add', 'Microsoft.Component.VC.Runtime.UCRTSDK', ` + '--add', 'Microsoft.VisualStudio.Component.Windows10SDK.17763', ` + '--quiet', '--norestart', '--nocache' ` + -NoNewWindow -Wait; ` + Remove-Item -Force vs_buildtools.exe; ` + Remove-Item -Force -Recurse \"${Env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\"; ` + Remove-Item -Force -Recurse ${Env:TEMP}\*; ` + Remove-Item -Force -Recurse \"${Env:ProgramData}\Package Cache\"; ` + [Environment]::SetEnvironmentVariable('BAZEL_VC', \"${Env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\VC\", [System.EnvironmentVariableTarget]::Machine) -CMD ["cmd.exe"] \ No newline at end of file +# Install Python +RUN Invoke-WebRequest -UseBasicParsing https://www.python.org/ftp/python/3.5.1/python-3.5.1.exe -OutFile python-3.5.1.exe; ` + Start-Process python-3.5.1.exe -ArgumentList '/quiet InstallAllUsers=1 PrependPath=1' -Wait; ` + Remove-Item -Force python-3.5.1.exe + +CMD ["cmd.exe"] diff --git a/.codefresh/bazel.rc b/.codefresh/bazel.rc index 5a2fa5ca6d..1dd9c1e81f 100644 --- a/.codefresh/bazel.rc +++ b/.codefresh/bazel.rc @@ -2,11 +2,17 @@ # We do this by copying this file to /etc/bazel.bazelrc at the start of the build. # See documentation in /docs/BAZEL.md -# Save downloaded repositories in a location that can be cached by CodeFresh and shared between -# builds. This helps speed up the analysis time significantly with Bazel managed node dependencies -# on the CI. +# Save built files and downloaded repositories in a location that can be cached by CodeFresh and +# shared between builds. This helps speed up the analysis time significantly with Bazel managed node +# dependencies on the CI. # https://codefresh.io/docs/docs/configure-ci-cd-pipeline/introduction-to-codefresh-pipelines/#caching-the-artifacts-of-your-build-system build --repository_cache=C:/codefresh/volume/bazel_repository_cache +# Setting the output_base to a Docker volume is currently broken because of a Docker bug on Windows: +# https://github.com/moby/moby/issues/37024 +# This affects Bazel because bazel_output_base\external\bazel_tools is an absolute path junction. +# When its fixed we can uncomment this line, and use a different output_base for Ivy tests (they +# use a separate compiler and destructively replace the cache). +# startup --output_base=C:/codefresh/volume/bazel_output_base # Don't be spammy in the logs # TODO(gmagolan): Hide progress again once build performance improves @@ -23,20 +29,10 @@ build --announce_rc # Bazel doesn't calculate the memory ceiling correctly when running under Docker. # Limit Bazel to consuming resources that fit in CodeFresh VMs # TODO(filipesilva): determine the correct memory limit -build --local_resources=8000,8.0,1.0 +build --local_resources=10240,8.0,1.0 # Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309 test --flaky_test_attempts=2 # More details on failures build --verbose_failures=true - -# Include PATH in Windows build/tests -# https://github.com/bazelbuild/rules_typescript/pull/356 -build --action_env=PATH -test --action_env=PATH --test_env=PATH - -# Exclude tests known to not work on Windows. - -# Chrome web tests are currently broken. -test --test_tag_filters=-browser:chromium-local diff --git a/.codefresh/codefresh.yml b/.codefresh/codefresh.yml index 7316835c66..0c738e78ed 100644 --- a/.codefresh/codefresh.yml +++ b/.codefresh/codefresh.yml @@ -2,6 +2,7 @@ version: '1.0' steps: BuildImage: + title: Build Docker image type: build image_name: node-bazel-windows working_directory: ./.codefresh @@ -12,7 +13,7 @@ steps: dockerfile: ./Dockerfile.win-1809 RunTests: - title: Run Example + title: Run Bazel tests image: ${{BuildImage}} commands: # Install dependencies @@ -20,7 +21,8 @@ steps: # Add Bazel CI config - copy .codefresh\bazel.rc %ProgramData%\bazel.bazelrc # Run tests - - yarn bazel test //tools/ts-api-guardian:all //packages/language-service/test - - yarn test-ivy-aot //packages/animations/test //packages/common/test //packages/forms/test //packages/http/test //packages/platform-browser/test //packages/platform-browser-dynamic/test //packages/router/test - - yarn bazel test //tools/public_api_guard/... - - yarn bazel test //packages/compiler-cli/integrationtest:integrationtest //packages/compiler-cli/test/compliance:compliance + # At the moment 'browser:chromium-local' are broken in CI while locally they work + # VE + - yarn bazel test --build_tag_filters=-ivy-only --test_tag_filters=-ivy-only,-browser:chromium-local //... + # Ivy + - yarn bazel test --define=compile=aot --build_tag_filters=-no-ivy-aot,-fixme-ivy-aot --test_tag_filters=-no-ivy-aot,-fixme-ivy-aot,-browser:chromium-local //... diff --git a/.codefresh/fix-msys64.cmd b/.codefresh/fix-msys64.cmd deleted file mode 100644 index 3869596d08..0000000000 --- a/.codefresh/fix-msys64.cmd +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -REM Fix for https://github.com/docker/for-win/issues/2920 -REM echo "Fixing msys64 folder..." -REM Touch all .dll files inside C:\msys64\ -forfiles /p C:\msys64\ /s /m *.dll /c "cmd /c Copy /B @path+,, >NUL" -REM echo "Fixed msys64 folder." \ No newline at end of file diff --git a/.devcontainer/recommended-Dockerfile b/.devcontainer/recommended-Dockerfile new file mode 100644 index 0000000000..a004714591 --- /dev/null +++ b/.devcontainer/recommended-Dockerfile @@ -0,0 +1,22 @@ +# Image metadata and config. +FROM circleci/node:10-browsers + +LABEL name="Angular dev environment" \ + description="This image can be used to create a dev environment for building Angular." \ + vendor="angular" \ + version="1.0" + +EXPOSE 4000 4200 4433 5000 8080 9876 + + +# Switch to `root` (CircleCI images use `circleci` as the user). +USER root + + +# Configure `Node.js`/`npm` and install utilities. +RUN npm config --global set user root +RUN npm install --global yarn@1.13.0 # This needs to be in sync with what we use on CI. + + +# Go! (And keep going.) +CMD ["tail", "--follow", "/dev/null"] diff --git a/.devcontainer/recommended-devcontainer.json b/.devcontainer/recommended-devcontainer.json new file mode 100644 index 0000000000..fa6320461b --- /dev/null +++ b/.devcontainer/recommended-devcontainer.json @@ -0,0 +1,16 @@ +// Reference: https://code.visualstudio.com/docs/remote/containers#_devcontainerjson-reference +{ + "name": "Angular dev container", + "dockerFile": "Dockerfile", + "appPort": [4000, 4200, 4433, 5000, 8080, 9876], + "postCreateCommand": "yarn install", + "extensions": [ + "devondcarew.bazel-code", + "gkalpak.aio-docs-utils", + "ms-vscode.vscode-typescript-tslint-plugin", + "xaver.clang-format", + // The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory). + //"angular.ng-template", + //"dbaeumer.vscode-eslint", + ], +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c7fe7b236..730f816c81 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -661,6 +661,7 @@ /packages/common/upgrade/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /packages/examples/upgrade/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/guide/upgrade.md @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes +/aio/content/examples/upgrade-lazy-load-ajs/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/examples/upgrade-module/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/images/guide/upgrade/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/examples/upgrade-phonecat-1-typescript/** @angular/fw-upgrade @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes @@ -811,6 +812,7 @@ testing/** @angular/fw-test /aio/content/guide/releases.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/guide/updating.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes /aio/content/guide/workspace-config.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes +/aio/content/guide/deprecations.md @angular/fw-docs-packaging @angular/framework-global-approvers @angular/framework-global-approvers-for-docs-only-changes @@ -855,6 +857,7 @@ testing/** @angular/fw-test /.buildkite/** @angular/fw-dev-infra /.circleci/** @angular/fw-dev-infra /.codefresh/** @angular/fw-dev-infra +/.devcontainer/** @angular/fw-dev-infra /.github/** @angular/fw-dev-infra /.vscode/** @angular/fw-dev-infra /docs/BAZEL.md @angular/fw-dev-infra diff --git a/.gitignore b/.gitignore index fc7de6bcee..68ebb8b13a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ tools/gulp-tasks/cldr/cldr-data/ pubspec.lock .c9 .idea/ +.devcontainer/* +!.devcontainer/recommended-devcontainer.json +!.devcontainer/recommended-Dockerfile .settings/ .vscode/launch.json .vscode/settings.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5d121aa51d..30c3aca137 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,5 +8,8 @@ "gkalpak.aio-docs-utils", "ms-vscode.vscode-typescript-tslint-plugin", "xaver.clang-format", + // The following extensions are useful when working on angular.io (i.e. inside the `aio/` directory). + //"angular.ng-template", + //"dbaeumer.vscode-eslint", ], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4420875f3f..66cb186484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,320 @@ + +# [8.1.0-beta.0](https://github.com/angular/angular/compare/8.0.0...8.1.0-beta.0) (2019-05-30) + + +### Bug Fixes + +* **bazel:** allow ts_library interop with list-typed inputs ([#30600](https://github.com/angular/angular/issues/30600)) ([3125376](https://github.com/angular/angular/commit/3125376)) +* **bazel:** Bump ibazel to 0.10.1 for windows fixes ([#30196](https://github.com/angular/angular/issues/30196)) ([1353bf0](https://github.com/angular/angular/commit/1353bf0)) +* **bazel:** Directly spawn native Bazel binary ([#30306](https://github.com/angular/angular/issues/30306)) ([2a0f497](https://github.com/angular/angular/commit/2a0f497)) +* **bazel:** Disable sandbox on Mac OS ([#30460](https://github.com/angular/angular/issues/30460)) ([b6b1aec](https://github.com/angular/angular/commit/b6b1aec)) +* **bazel:** Exclude common/upgrade* in metadata.tsconfig.json ([#30133](https://github.com/angular/angular/issues/30133)) ([1f4c380](https://github.com/angular/angular/commit/1f4c380)) +* **bazel:** ng test should run specific ts_web_test_suite ([#30526](https://github.com/angular/angular/issues/30526)) ([e688e02](https://github.com/angular/angular/commit/e688e02)) +* **bazel:** pass correct arguments to http_server in Windows ([#30346](https://github.com/angular/angular/issues/30346)) ([3aff79c](https://github.com/angular/angular/commit/3aff79c)), closes [#29785](https://github.com/angular/angular/issues/29785) +* **bazel:** update peerDep ranges ([#30155](https://github.com/angular/angular/issues/30155)) ([4ae0ee8](https://github.com/angular/angular/commit/4ae0ee8)) +* **bazel:** Use existing npm/yarn lock files ([#30438](https://github.com/angular/angular/issues/30438)) ([ff29ccc](https://github.com/angular/angular/commit/ff29ccc)) +* **compiler-cli:** log ngcc skipping messages as debug instead of info ([#30232](https://github.com/angular/angular/issues/30232)) ([60a8888](https://github.com/angular/angular/commit/60a8888)) +* **core:** consistently use ng:/// for sourcemap URLs ([#29826](https://github.com/angular/angular/issues/29826)) ([392473e](https://github.com/angular/angular/commit/392473e)) +* **core:** CSS sanitizer now allows parens in file names ([#30322](https://github.com/angular/angular/issues/30322)) ([728db88](https://github.com/angular/angular/commit/728db88)) +* **core:** fix interpolate identifier in AOT ([#30243](https://github.com/angular/angular/issues/30243)) ([30d1f29](https://github.com/angular/angular/commit/30d1f29)) +* **core:** migrations not always migrating all files ([#30269](https://github.com/angular/angular/issues/30269)) ([349935a](https://github.com/angular/angular/commit/349935a)) +* **core:** remove deprecated `TestBed.deprecatedOverrideProvider` API ([#30576](https://github.com/angular/angular/issues/30576)) ([a96976e](https://github.com/angular/angular/commit/a96976e)) +* **core:** require 'static' flag on queries in typings ([#30639](https://github.com/angular/angular/issues/30639)) ([84dd267](https://github.com/angular/angular/commit/84dd267)) +* **core:** static-query migration errors not printed properly ([#30458](https://github.com/angular/angular/issues/30458)) ([6ceb903](https://github.com/angular/angular/commit/6ceb903)) +* **core:** static-query migration fails with default parameter values ([#30269](https://github.com/angular/angular/issues/30269)) ([6357d4a](https://github.com/angular/angular/commit/6357d4a)) +* **core:** static-query migration should gracefully exit if AOT compiler throws ([#30269](https://github.com/angular/angular/issues/30269)) ([509352f](https://github.com/angular/angular/commit/509352f)) +* **core:** static-query migration should handle queries on accessors ([#30327](https://github.com/angular/angular/issues/30327)) ([0ffdb48](https://github.com/angular/angular/commit/0ffdb48)) +* **core:** static-query migration should not fallback to test strategy ([#30458](https://github.com/angular/angular/issues/30458)) ([0cdf598](https://github.com/angular/angular/commit/0cdf598)) +* **core:** static-query migration should not prompt if no queries are used ([#30254](https://github.com/angular/angular/issues/30254)) ([4c12d74](https://github.com/angular/angular/commit/4c12d74)) +* **core:** static-query usage migration strategy should detect ambiguous query usage ([#30215](https://github.com/angular/angular/issues/30215)) ([8d3365e](https://github.com/angular/angular/commit/8d3365e)) +* **core:** temporarily remove [@deprecated](https://github.com/deprecated) jsdoc tag for a TextBed.get overload ([#30514](https://github.com/angular/angular/issues/30514)) ([f6bf892](https://github.com/angular/angular/commit/f6bf892)), closes [#29290](https://github.com/angular/angular/issues/29290) [#29905](https://github.com/angular/angular/issues/29905) +* **language-service:** Remove tsserverlibrary from rollup globals ([#30123](https://github.com/angular/angular/issues/30123)) ([124e497](https://github.com/angular/angular/commit/124e497)) +* **router:** ensure `history.state` is set in `eager` update mode ([#30154](https://github.com/angular/angular/issues/30154)) ([b40f6f3](https://github.com/angular/angular/commit/b40f6f3)) +* **router:** ensure navigations start with the current URL value incase redirect is skipped ([#30344](https://github.com/angular/angular/issues/30344)) ([0fd9d08](https://github.com/angular/angular/commit/0fd9d08)), closes [#30340](https://github.com/angular/angular/issues/30340) [#30160](https://github.com/angular/angular/issues/30160) +* **router:** fix a problem with router not responding to back button ([#30160](https://github.com/angular/angular/issues/30160)) ([3327bd8](https://github.com/angular/angular/commit/3327bd8)) +* **router:** IE 11 bug can break URL unification when comparing objects ([#30393](https://github.com/angular/angular/issues/30393)) ([197584d](https://github.com/angular/angular/commit/197584d)) +* **router:** type cast correctly for IE 11 bug breaking URL Unification when comparing objects ([#30464](https://github.com/angular/angular/issues/30464)) ([53f3564](https://github.com/angular/angular/commit/53f3564)) + + +### Features + +* **bazel:** use `rbe_autoconfig()` and new container. ([#29336](https://github.com/angular/angular/issues/29336)) ([9abf114](https://github.com/angular/angular/commit/9abf114)) +* **common:** add ability to watch for AngularJS URL updates through `onUrlChange` hook ([#30466](https://github.com/angular/angular/issues/30466)) ([1aff524](https://github.com/angular/angular/commit/1aff524)) +* **common:** stricter types for `SlicePipe` ([#30156](https://github.com/angular/angular/issues/30156)) ([95830ee](https://github.com/angular/angular/commit/95830ee)) +* **core:** deprecate integration with the Web Tracing Framework (WTF) ([#30642](https://github.com/angular/angular/issues/30642)) ([f310a59](https://github.com/angular/angular/commit/f310a59)) +* **language-service:** Implement `definitionAndBoundSpan` ([#30125](https://github.com/angular/angular/issues/30125)) ([f491673](https://github.com/angular/angular/commit/f491673)) +* **platform-webworker:** deprecate platform-webworker ([#30642](https://github.com/angular/angular/issues/30642)) ([ccc76f7](https://github.com/angular/angular/commit/ccc76f7)) + + + + +# [8.0.0](https://github.com/angular/angular/compare/8.0.0-rc.5...8.0.0) (2019-05-28) + + +### Features + +* add support for TypeScript 3.4 (and drop older versions) ([#29372](https://github.com/angular/angular/issues/29372)) ([ef85336](https://github.com/angular/angular/commit/ef85336)) +* **common:** add ability to watch for AngularJS URL updates through `onUrlChange` hook ([#30466](https://github.com/angular/angular/issues/30466)) ([8022d36](https://github.com/angular/angular/commit/8022d36)) +* **common:** stricter types for `SlicePipe` ([#30156](https://github.com/angular/angular/issues/30156)) ([722b2fa](https://github.com/angular/angular/commit/722b2fa)) +* **bazel:** use `rbe_autoconfig()` and new container ([#29336](https://github.com/angular/angular/issues/29336)) ([e562acc](https://github.com/angular/angular/commit/e562acc)) +* **common:** add @angular/common/upgrade package for `$location`-related APIs ([#30055](https://github.com/angular/angular/issues/30055)) ([152d99e](https://github.com/angular/angular/commit/152d99e)) +* **common:** add ability to retrieve the state from `Location` service ([#30055](https://github.com/angular/angular/issues/30055)) ([b44b143](https://github.com/angular/angular/commit/b44b143)) +* **common:** add ability to track all location changes ([#30055](https://github.com/angular/angular/issues/30055)) ([3a9cf3f](https://github.com/angular/angular/commit/3a9cf3f)) +* **common:** add APIs to read component pieces of URL ([#30055](https://github.com/angular/angular/issues/30055)) ([b635fe8](https://github.com/angular/angular/commit/b635fe8)) +* **common:** add `MockPlatformLocation` to enable more robust testing of `Location` services ([#30055](https://github.com/angular/angular/issues/30055)) ([d0672c2](https://github.com/angular/angular/commit/d0672c2)) +* **common:** add `UrlCodec` type for use with upgrade applications ([#30055](https://github.com/angular/angular/issues/30055)) ([ec455e1](https://github.com/angular/angular/commit/ec455e1)) +* **common:** provide replacement for AngularJS $location service ([#30055](https://github.com/angular/angular/issues/30055)) ([4277600](https://github.com/angular/angular/commit/4277600)) +* remove deprecated `DOCUMENT` token from platform-browser ([#28117](https://github.com/angular/angular/issues/28117)) ([3a9d247](https://github.com/angular/angular/commit/3a9d247)) +* **compiler:** support skipping leading trivia in template source-maps ([#30095](https://github.com/angular/angular/issues/30095)) ([304a12f](https://github.com/angular/angular/commit/304a12f)) +* **core:** add missing ARIA attributes to html sanitizer ([#29685](https://github.com/angular/angular/issues/29685)) ([909557d](https://github.com/angular/angular/commit/909557d)), closes [#26815](https://github.com/angular/angular/issues/26815) +* **router:** deprecate loadChildren:string ([#30073](https://github.com/angular/angular/issues/30073)) ([c61df39](https://github.com/angular/angular/commit/c61df39)) +* **service-worker:** allow configuring when the SW is registered ([#21842](https://github.com/angular/angular/issues/21842)) ([4cfba58](https://github.com/angular/angular/commit/4cfba58)), closes [#20970](https://github.com/angular/angular/issues/20970) +* **service-worker:** expose `SwRegistrationOptions` token to allow runtime config ([#21842](https://github.com/angular/angular/issues/21842)) ([39c0152](https://github.com/angular/angular/commit/39c0152)) +* **service-worker:** support bypassing SW with specific header/query param ([#30010](https://github.com/angular/angular/issues/30010)) ([6200732](https://github.com/angular/angular/commit/6200732)), closes [#21191](https://github.com/angular/angular/issues/21191) +* **compiler-cli:** export tooling definitions ([#29929](https://github.com/angular/angular/issues/29929)) ([e1f51ea](https://github.com/angular/angular/commit/e1f51ea)) +* **compiler-cli:** lower some exported expressions ([#30038](https://github.com/angular/angular/issues/30038)) ([8e73f9b](https://github.com/angular/angular/commit/8e73f9b)) +* **core:** add schematics to move deprecated `DOCUMENT` import ([#29950](https://github.com/angular/angular/issues/29950)) ([645e305](https://github.com/angular/angular/commit/645e305)) +* **bazel:** update the build to use the new architect api ([#29720](https://github.com/angular/angular/issues/29720)) ([902a53a](https://github.com/angular/angular/commit/902a53a)) +* remove @angular/http dependency from @angular/platform-server ([#29408](https://github.com/angular/angular/issues/29408)) ([9745f55](https://github.com/angular/angular/commit/9745f55)) +* **compiler-cli:** ngcc - make logging more configurable ([#29591](https://github.com/angular/angular/issues/29591)) ([8d3d75e](https://github.com/angular/angular/commit/8d3d75e)) +* **core:** Add `AbstractType` interface ([#29295](https://github.com/angular/angular/issues/29295)) ([afd4a4e](https://github.com/angular/angular/commit/afd4a4e)), closes [#26491](https://github.com/angular/angular/issues/26491) +* **core:** template-var-assignment update schematic ([#29608](https://github.com/angular/angular/issues/29608)) ([7c8f4e3](https://github.com/angular/angular/commit/7c8f4e3)) +* **bazel:** Upgrade rules_nodejs and rules_sass ([#29388](https://github.com/angular/angular/issues/29388)) ([d6d081e](https://github.com/angular/angular/commit/d6d081e)) +* **service-worker:** support multiple apps on different subpaths of a domain ([#27080](https://github.com/angular/angular/issues/27080)) ([e721c08](https://github.com/angular/angular/commit/e721c08)), closes [#21388](https://github.com/angular/angular/issues/21388) +* **bazel:** Eject Bazel ([#29167](https://github.com/angular/angular/issues/29167)) ([36a1550](https://github.com/angular/angular/commit/36a1550)) +* **bazel:** Hide Bazel files in Bazel builder ([#29110](https://github.com/angular/angular/issues/29110)) ([7060d90](https://github.com/angular/angular/commit/7060d90)) +* **forms:** clear (remove all) components from a FormArray ([#28918](https://github.com/angular/angular/issues/28918)) ([a68b1a1](https://github.com/angular/angular/commit/a68b1a1)), closes [#18531](https://github.com/angular/angular/issues/18531) +* **platform-server:** wait on returned `BEFORE_APP_SERIALIZED` promises ([#29120](https://github.com/angular/angular/issues/29120)) ([7102ea8](https://github.com/angular/angular/commit/7102ea8)) + + +### Bug Fixes + +* **bazel:** allow `ts_library` interop with list-typed inputs ([#30600](https://github.com/angular/angular/issues/30600)) ([bf38df4](https://github.com/angular/angular/commit/bf38df4)) +* **bazel:** Disable sandbox on Mac OS ([#30460](https://github.com/angular/angular/issues/30460)) ([3de26a8](https://github.com/angular/angular/commit/3de26a8)) +* **bazel:** ng test should run specific ts_web_test_suite ([#30526](https://github.com/angular/angular/issues/30526)) ([8bc4da8](https://github.com/angular/angular/commit/8bc4da8)) +* **core:** remove deprecated `TestBed.deprecatedOverrideProvider` API ([#30576](https://github.com/angular/angular/issues/30576)) ([5a46f94](https://github.com/angular/angular/commit/5a46f94)) +* **core:** require 'static' flag on queries in typings ([#30641](https://github.com/angular/angular/issues/30641)) ([c8af830](https://github.com/angular/angular/commit/c8af830)) +* **core:** temporarily remove [@deprecated](https://github.com/deprecated) jsdoc tag for a `TextBed.get` overload ([#30514](https://github.com/angular/angular/issues/30514)) ([561e01d](https://github.com/angular/angular/commit/561e01d)), closes [#29290](https://github.com/angular/angular/issues/29290) [#29905](https://github.com/angular/angular/issues/29905) +* **router:** type cast correctly for IE 11 bug breaking URL Unification when comparing objects ([#30464](https://github.com/angular/angular/issues/30464)) ([32daa93](https://github.com/angular/angular/commit/32daa93)) +* **bazel:** Directly spawn native Bazel binary ([#30306](https://github.com/angular/angular/issues/30306)) ([d1fcc2b](https://github.com/angular/angular/commit/d1fcc2b)) +* **bazel:** pass correct arguments to http_server in Windows ([#30346](https://github.com/angular/angular/issues/30346)) ([71eba45](https://github.com/angular/angular/commit/71eba45)), closes [#29785](https://github.com/angular/angular/issues/29785) +* **bazel:** Use existing npm/yarn lock files ([#30438](https://github.com/angular/angular/issues/30438)) ([3136d9f](https://github.com/angular/angular/commit/3136d9f)) +* **compiler:** ensure strict mode when evaluating in JIT ([#30122](https://github.com/angular/angular/issues/30122)) ([192f108](https://github.com/angular/angular/commit/192f108)) +* **core:** migrations not always migrating all files ([#30269](https://github.com/angular/angular/issues/30269)) ([e8ceae1](https://github.com/angular/angular/commit/e8ceae1)) +* **core:** static-query migration errors not printed properly ([#30458](https://github.com/angular/angular/issues/30458)) ([fde3f46](https://github.com/angular/angular/commit/fde3f46)) +* **core:** static-query migration fails with default parameter values ([#30269](https://github.com/angular/angular/issues/30269)) ([c3246e6](https://github.com/angular/angular/commit/c3246e6)) +* **core:** static-query migration should gracefully exit if AOT compiler throws ([#30269](https://github.com/angular/angular/issues/30269)) ([a71d8a8](https://github.com/angular/angular/commit/a71d8a8)) +* **core:** static-query migration should handle queries on accessors ([#30327](https://github.com/angular/angular/issues/30327)) ([dd299f9](https://github.com/angular/angular/commit/dd299f9)) +* **core:** static-query migration should not fallback to test strategy ([#30458](https://github.com/angular/angular/issues/30458)) ([0fa48e8](https://github.com/angular/angular/commit/0fa48e8)) +* **core:** static-query migration should not prompt if no queries are used ([#30254](https://github.com/angular/angular/issues/30254)) ([12fb639](https://github.com/angular/angular/commit/12fb639)) +* **core:** static-query usage migration strategy should detect ambiguous query usage ([#30215](https://github.com/angular/angular/issues/30215)) ([e295c6a](https://github.com/angular/angular/commit/e295c6a)) +* **router:** ensure navigations start with the current URL value incase redirect is skipped ([#30344](https://github.com/angular/angular/issues/30344)) ([9b88920](https://github.com/angular/angular/commit/9b88920)), closes [#30340](https://github.com/angular/angular/issues/30340) [#30160](https://github.com/angular/angular/issues/30160) +* **router:** IE 11 bug can break URL unification when comparing objects ([#30393](https://github.com/angular/angular/issues/30393)) ([c383491](https://github.com/angular/angular/commit/c383491)) +* **bazel:** Bump ibazel to 0.10.1 for Windows fixes ([#30196](https://github.com/angular/angular/issues/30196)) ([9f68c35](https://github.com/angular/angular/commit/9f68c35)) +* **compiler-cli:** log ngcc skipping messages as debug instead of info ([#30232](https://github.com/angular/angular/issues/30232)) ([548b003](https://github.com/angular/angular/commit/548b003)) +* **core:** fix interpolate identifier in AOT ([#30243](https://github.com/angular/angular/issues/30243)) ([3fe3a84](https://github.com/angular/angular/commit/3fe3a84)) +* **router:** ensure `history.state` is set in `eager` update mode ([#30154](https://github.com/angular/angular/issues/30154)) ([9720227](https://github.com/angular/angular/commit/9720227)) +* **router:** fix a problem with router not responding to back button ([#30160](https://github.com/angular/angular/issues/30160)) ([132f01c](https://github.com/angular/angular/commit/132f01c)) +* **language-service:** Remove tsserverlibrary from rollup globals ([#30123](https://github.com/angular/angular/issues/30123)) ([b706800](https://github.com/angular/angular/commit/b706800)) +* disable injectable-pipe migration ([#30180](https://github.com/angular/angular/issues/30180)) ([4b2fcfd](https://github.com/angular/angular/commit/4b2fcfd)) +* **bazel:** Exclude common/upgrade* in metadata.tsconfig.json ([#30133](https://github.com/angular/angular/issues/30133)) ([6711f22](https://github.com/angular/angular/commit/6711f22)) +* **bazel:** update peerDep ranges ([#30155](https://github.com/angular/angular/issues/30155)) ([6067583](https://github.com/angular/angular/commit/6067583)) +* **bazel:** make name param in ng add optional ([#30074](https://github.com/angular/angular/issues/30074)) ([0b5f480](https://github.com/angular/angular/commit/0b5f480)) +* **bazel:** Make sure only single copy of @angular/bazel is installed ([#30072](https://github.com/angular/angular/issues/30072)) ([2905bf5](https://github.com/angular/angular/commit/2905bf5)) +* **bazel:** transitive npm deps in ng_module ([#30065](https://github.com/angular/angular/issues/30065)) ([61365a9](https://github.com/angular/angular/commit/61365a9)) +* **common:** add upgrade sub-package to `ng_package` rule for @angular/common ([#30117](https://github.com/angular/angular/issues/30117)) ([6de4cbd](https://github.com/angular/angular/commit/6de4cbd)), closes [#30055](https://github.com/angular/angular/issues/30055) [#30116](https://github.com/angular/angular/issues/30116) +* **common:** adjust `MockPlatformLocation` to set state to new object ([#30055](https://github.com/angular/angular/issues/30055)) ([825efa8](https://github.com/angular/angular/commit/825efa8)) +* **compiler:** Fix compiler crash due to isSkipSelf of null ([#30075](https://github.com/angular/angular/issues/30075)) ([28fd5ab](https://github.com/angular/angular/commit/28fd5ab)) +* **upgrade:** do not break if `onMicrotaskEmpty` emits while a `$digest` is in progress ([#29794](https://github.com/angular/angular/issues/29794)) ([0ddf2e7](https://github.com/angular/angular/commit/0ddf2e7)), closes [#24680](https://github.com/angular/angular/issues/24680) [/github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12](https://github.com//github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts/issues/L12) [#24680](https://github.com/angular/angular/issues/24680) +* **bazel:** do not typecheck core schematic files ([#29876](https://github.com/angular/angular/issues/29876)) ([2ba799d](https://github.com/angular/angular/commit/2ba799d)) +* **bazel:** restore `ng build --prod` ([#30005](https://github.com/angular/angular/issues/30005)) ([96a8289](https://github.com/angular/angular/commit/96a8289)) +* **common:** prevent repeated application of `HttpParams` mutations ([#29045](https://github.com/angular/angular/issues/29045)) ([8e8e89a](https://github.com/angular/angular/commit/8e8e89a)), closes [#20430](https://github.com/angular/angular/issues/20430) +* **common:** async pipe will properly check when it receives an NaN value from an observable ([#22305](https://github.com/angular/angular/issues/22305)) ([3f6bf6d](https://github.com/angular/angular/commit/3f6bf6d)), closes [#15721](https://github.com/angular/angular/issues/15721) +* **core:** don't include a local `EventListener` in typings ([#29809](https://github.com/angular/angular/issues/29809)) ([4bde40f](https://github.com/angular/angular/commit/4bde40f)), closes [#29806](https://github.com/angular/angular/issues/29806) +* **core:** use shakeable global definitions ([#29929](https://github.com/angular/angular/issues/29929)) ([e5905bb](https://github.com/angular/angular/commit/e5905bb)) +* **language-service:** Use proper types instead of any ([#29942](https://github.com/angular/angular/issues/29942)) ([1a56cd5](https://github.com/angular/angular/commit/1a56cd5)) +* **bazel:** Install packages after `ng add` when invoked independently ([#29852](https://github.com/angular/angular/issues/29852)) ([bd2ce9c](https://github.com/angular/angular/commit/bd2ce9c)) +* **compiler-cli:** pass config path to `ts.parseJsonConfigFileContent` ([#29872](https://github.com/angular/angular/issues/29872)) ([86a3f90](https://github.com/angular/angular/commit/86a3f90)) +* **router:** support non-NgFactory promise in loadChildren typings ([#29832](https://github.com/angular/angular/issues/29832)) ([2bfb6a0](https://github.com/angular/angular/commit/2bfb6a0)) +* **bazel:** add `configuration_env_vars = ["compile"]` to generated `@npm//@angular/bazel/bin:ngc-wrapped` `nodejs_binary` ([#29694](https://github.com/angular/angular/issues/29694)) ([2e66ddf](https://github.com/angular/angular/commit/2e66ddf)) +* **bazel:** docs formatting ([#29817](https://github.com/angular/angular/issues/29817)) ([cc2e4b6](https://github.com/angular/angular/commit/cc2e4b6)) +* **bazel:** remove karma-jasmine from `ts_web_test_suite` ([#29695](https://github.com/angular/angular/issues/29695)) ([2bd9214](https://github.com/angular/angular/commit/2bd9214)) +* **bazel:** support running ng-add on minimal applications ([#29681](https://github.com/angular/angular/issues/29681)) ([9810c6c](https://github.com/angular/angular/commit/9810c6c)), closes [#29680](https://github.com/angular/angular/issues/29680) +* **common:** add `@Injectable()` to common pipes ([#29834](https://github.com/angular/angular/issues/29834)) ([387fbb8](https://github.com/angular/angular/commit/387fbb8)) +* **compiler-cli:** ensure `LogicalProjectPaths` always start with a slash ([#29627](https://github.com/angular/angular/issues/29627)) ([e02684e](https://github.com/angular/angular/commit/e02684e)) +* **core:** add missing migration to npm package ([#29705](https://github.com/angular/angular/issues/29705)) ([96b76dc](https://github.com/angular/angular/commit/96b76dc)) +* **core:** call `ngOnDestroy` for tree-shakeable providers ([#28943](https://github.com/angular/angular/issues/28943)) ([30b0442](https://github.com/angular/angular/commit/30b0442)), closes [#28927](https://github.com/angular/angular/issues/28927) +* **core:** Deprecate `TestBed.get(...):any` ([#29290](https://github.com/angular/angular/issues/29290)) ([609024f](https://github.com/angular/angular/commit/609024f)), closes [#13785](https://github.com/angular/angular/issues/13785) [#26491](https://github.com/angular/angular/issues/26491) +* **core:** resolve ts compile issues due to lenient tsconfig ([#29843](https://github.com/angular/angular/issues/29843)) ([54058ba](https://github.com/angular/angular/commit/54058ba)) +* **platform-browser:** insert `APP_ID` in styles, contentAttr and hostAttr ([#17745](https://github.com/angular/angular/issues/17745)) ([712d60e](https://github.com/angular/angular/commit/712d60e)) +* **bazel:** Update schematics to support routing ([#29548](https://github.com/angular/angular/issues/29548)) ([401b8ee](https://github.com/angular/angular/commit/401b8ee)) +* **bazel:** use `//:tsconfig.json` as the default for `ng_module` ([#29670](https://github.com/angular/angular/issues/29670)) ([b14537a](https://github.com/angular/angular/commit/b14537a)) +* **compiler-cli:** ngcc - cope with processing entry-points multiple times ([#29657](https://github.com/angular/angular/issues/29657)) ([6b39c9c](https://github.com/angular/angular/commit/6b39c9c)) +* **core:** static-query schematic should detect static queries in getters. ([#29609](https://github.com/angular/angular/issues/29609)) ([33016b8](https://github.com/angular/angular/commit/33016b8)) +* **common:** escape query selector used when anchor scrolling ([#29577](https://github.com/angular/angular/issues/29577)) ([7671c73](https://github.com/angular/angular/commit/7671c73)), closes [#28193](https://github.com/angular/angular/issues/28193) +* **router:** adjust setting navigationTransition when a new navigation cancels an existing one ([#29636](https://github.com/angular/angular/issues/29636)) ([e884c0c](https://github.com/angular/angular/commit/e884c0c)), closes [#29389](https://github.com/angular/angular/issues/29389) [#29590](https://github.com/angular/angular/issues/29590) +* **bazel:** allow `ng_module` users to set `createExternalSymbolFactoryReexports` ([#29459](https://github.com/angular/angular/issues/29459)) ([21be0fb](https://github.com/angular/angular/commit/21be0fb)) +* **bazel:** workaround problem reading summary files from node_modules ([#29459](https://github.com/angular/angular/issues/29459)) ([769d960](https://github.com/angular/angular/commit/769d960)) +* **compiler:** inherit param types when class has a constructor which takes no declared parameters and delegates up ([#29232](https://github.com/angular/angular/issues/29232)) ([0007564](https://github.com/angular/angular/commit/0007564)) +* **core:** parse incorrect ML open tag as text ([#29328](https://github.com/angular/angular/issues/29328)) ([dafbbf8](https://github.com/angular/angular/commit/dafbbf8)), closes [#29231](https://github.com/angular/angular/issues/29231) +* **core:** static-query schematic should detect queries in `ngDoCheck` and `ngOnChanges` ([#29492](https://github.com/angular/angular/issues/29492)) ([09fab58](https://github.com/angular/angular/commit/09fab58)) +* **router:** support `NgFactory` promise in loadChildren typings ([#29392](https://github.com/angular/angular/issues/29392)) ([26a8c59](https://github.com/angular/angular/commit/26a8c59)) +* **bazel:** correct regexp test for self-references in metadata ([#29346](https://github.com/angular/angular/issues/29346)) ([9d090cb](https://github.com/angular/angular/commit/9d090cb)) +* **bazel:** don't produce self-references in metadata ([#29317](https://github.com/angular/angular/issues/29317)) ([3facdeb](https://github.com/angular/angular/commit/3facdeb)), closes [#29315](https://github.com/angular/angular/issues/29315) +* **bazel:** fix strict null checks compile error in `packages/bazel/src/schematics/ng-add/index.ts` ([#29282](https://github.com/angular/angular/issues/29282)) ([9a7f560](https://github.com/angular/angular/commit/9a7f560)) +* **bazel:** Remove @angular/upgrade from dev dependencies ([#29319](https://github.com/angular/angular/issues/29319)) ([1db8bf3](https://github.com/angular/angular/commit/1db8bf3)) +* **bazel:** Support new e2e project layout ([#29318](https://github.com/angular/angular/issues/29318)) ([8ef690c](https://github.com/angular/angular/commit/8ef690c)) +* **bazel:** turn off pure call tree shaking for ng_package ([#29210](https://github.com/angular/angular/issues/29210)) ([4990b93](https://github.com/angular/angular/commit/4990b93)) +* **compiler-cli:** incorrect metadata bundle for multiple unnamed re-exports ([#29360](https://github.com/angular/angular/issues/29360)) ([105cfaf](https://github.com/angular/angular/commit/105cfaf)) +* **core:** don't wrap `` and `` elements into a required parent ([#29219](https://github.com/angular/angular/issues/29219)) ([f2dc32e](https://github.com/angular/angular/commit/f2dc32e)) +* **core:** parse incorrect ML open tag as text ([#29328](https://github.com/angular/angular/issues/29328)) ([4605df8](https://github.com/angular/angular/commit/4605df8)), closes [#29231](https://github.com/angular/angular/issues/29231) +* **bazel:** add missing binary path for api-extractor ([#29202](https://github.com/angular/angular/issues/29202)) ([df354d1](https://github.com/angular/angular/commit/df354d1)) +* **bazel:** ng build should produce prod bundle ([#29136](https://github.com/angular/angular/issues/29136)) ([14ce8a9](https://github.com/angular/angular/commit/14ce8a9)) +* **compiler:** ensure template is updated if an output is transformed ([#29041](https://github.com/angular/angular/issues/29041)) ([c7e4931](https://github.com/angular/angular/commit/c7e4931)) + + +### DEPRECATIONS + +* **core:** deprecate integration with the Web Tracing Framework (WTF) ([#30642](https://github.com/angular/angular/issues/30642)) ([b408445](https://github.com/angular/angular/commit/b408445)) +* **platform-webworker:** deprecate platform-webworker ([#30642](https://github.com/angular/angular/issues/30642)) ([361f181](https://github.com/angular/angular/commit/361f181)) + + +### BREAKING CHANGES + +* **bazel:** @bazel/typescript is now a peerDependency of @angular/bazel so users of @angular/bazel must add @bazel/typescript to their package.json +* **bazel:** `ng_module` now depends on a minimum of build_bazel_rules_nodejs 0.27.12 +* **core:** In Angular version 8, it's required that all `@ViewChild` and `@ContentChild` +queries have a `'static'` flag specifying whether the query is 'static' or +'dynamic'. The compiler previously sorted queries automatically, but in +8.0 developers are required to explicitly specify which behavior is wanted. +This is a temporary requirement as part of a migration; see +[static query migration guide](https://v8.angular.io/guide/static-query-migration) for more details. + + `@ViewChildren` and `@ContentChildren` queries are always dynamic, and so are + unaffected. + +* `TestBed.get()` has two signatures, one which is typed and another which accepts and returns `any`. The signature for `any` is deprecated; all usage of `TestBed.get()` should go through the typed API. This mainly affects string tokens +(which aren't supported) and abstract class tokens. + + Before: + + ```ts + TestBed.configureTestingModule({ + providers: [{provide: "stringToken", useValue: new Service()}], + }); + + let service = TestBed.get("stringToken"); // type any + ``` + + After: + + ```ts + const SERVICE_TOKEN = new InjectionToken("SERVICE_TOKEN"); + + TestBed.configureTestingModule({ + providers: [{provide: SERVICE_TOKEN, useValue: new Service()}], + }); + + let service = TestBed.get(SERVICE_TOKEN); // type Service + ``` + +* **core:** Certain elements (like `` or ``) require parent elements to be of a certain type by the HTML specification +(ex. `` can only be inside `` / ``). Before this change Angular template parser was auto-correcting +"invalid" HTML using the following rules: + - `` would be wrapped in `` if not inside ``, `` or ``; + - `` would be wrapped in `` if not inside ``. + + This mechanism of automatic wrapping / auto-correcting was problematic for several reasons: + - it is non-obvious and arbitrary (ex. there are more HTML elements that have rules for parent type); + - it is incorrect for cases where `` / `` are at the root of a component's content, ex.: + + ```html + + ... + + ``` + + In the above example the `` component could be "surprised" to see additional + `` elements inserted by Angular HTML parser. + +* **http:** The deprecated @angular/http package has been removed, the @angular/common/http package should be used instead. +For details on how to migrate, please refer to [the deprecations guide](https://angular.io/guide/deprecations#angularhttp). + + +* TypeScript 3.1 and 3.2 are no longer supported. + + Please update your TypeScript version to 3.4, as version 3.3 is also not supported. + + + +# [8.0.0-rc.5](https://github.com/angular/angular/compare/8.0.0-rc.4...8.0.0-rc.5) (2019-05-24) + + +### Bug Fixes + +* **bazel:** allow ts_library interop with list-typed inputs ([#30600](https://github.com/angular/angular/issues/30600)) ([bf38df4](https://github.com/angular/angular/commit/bf38df4)) +* **bazel:** Disable sandbox on Mac OS ([#30460](https://github.com/angular/angular/issues/30460)) ([3de26a8](https://github.com/angular/angular/commit/3de26a8)) +* **bazel:** ng test should run specific ts_web_test_suite ([#30526](https://github.com/angular/angular/issues/30526)) ([8bc4da8](https://github.com/angular/angular/commit/8bc4da8)) +* **core:** remove deprecated `TestBed.deprecatedOverrideProvider` API ([#30576](https://github.com/angular/angular/issues/30576)) ([5a46f94](https://github.com/angular/angular/commit/5a46f94)) +* **core:** require 'static' flag on queries in typings ([#30641](https://github.com/angular/angular/issues/30641)) ([c8af830](https://github.com/angular/angular/commit/c8af830)) +* **core:** temporarily remove [@deprecated](https://github.com/deprecated) jsdoc tag for a TextBed.get overload ([#30514](https://github.com/angular/angular/issues/30514)) ([561e01d](https://github.com/angular/angular/commit/561e01d)), closes [#29290](https://github.com/angular/angular/issues/29290) [#29905](https://github.com/angular/angular/issues/29905) +* **router:** type cast correctly for IE 11 bug breaking URL Unification when comparing objects ([#30464](https://github.com/angular/angular/issues/30464)) ([32daa93](https://github.com/angular/angular/commit/32daa93)) + + +### Features + +* **common:** add ability to watch for AngularJS URL updates through `onUrlChange` hook ([#30466](https://github.com/angular/angular/issues/30466)) ([8022d36](https://github.com/angular/angular/commit/8022d36)) +* **common:** stricter types for SlicePipe ([#30156](https://github.com/angular/angular/issues/30156)) ([722b2fa](https://github.com/angular/angular/commit/722b2fa)) + + +### DEPRECATIONS + +* **core:** deprecate integration with the Web Tracing Framework (WTF) ([#30642](https://github.com/angular/angular/issues/30642)) ([b408445](https://github.com/angular/angular/commit/b408445)) +* **platform-webworker:** deprecate platform-webworker ([#30642](https://github.com/angular/angular/issues/30642)) ([361f181](https://github.com/angular/angular/commit/361f181)) + + +### BREAKING CHANGES + +* **core:** In Angular version 8, it's required that all @ViewChild and @ContentChild +queries have a 'static' flag specifying whether the query is 'static' or +'dynamic'. The compiler previously sorted queries automatically, but in +8.0 developers are required to explicitly specify which behavior is wanted. +This is a temporary requirement as part of a migration; see +https://v8.angular.io/guide/static-query-migration for more details. + +@ViewChildren and @ContentChildren queries are always dynamic, and so are +unaffected. + + + + +# [8.0.0-rc.4](https://github.com/angular/angular/compare/8.0.0-rc.3...8.0.0-rc.4) (2019-05-15) + + +### Bug Fixes + +* **bazel:** Directly spawn native Bazel binary ([#30306](https://github.com/angular/angular/issues/30306)) ([d1fcc2b](https://github.com/angular/angular/commit/d1fcc2b)) +* **bazel:** pass correct arguments to http_server in Windows ([#30346](https://github.com/angular/angular/issues/30346)) ([71eba45](https://github.com/angular/angular/commit/71eba45)), closes [#29785](https://github.com/angular/angular/issues/29785) +* **bazel:** Use existing npm/yarn lock files ([#30438](https://github.com/angular/angular/issues/30438)) ([3136d9f](https://github.com/angular/angular/commit/3136d9f)) +* **compiler:** ensure strict mode when evaluating in JIT ([#30122](https://github.com/angular/angular/issues/30122)) ([192f108](https://github.com/angular/angular/commit/192f108)) +* **core:** migrations not always migrating all files ([#30269](https://github.com/angular/angular/issues/30269)) ([e8ceae1](https://github.com/angular/angular/commit/e8ceae1)) +* **core:** static-query migration errors not printed properly ([#30458](https://github.com/angular/angular/issues/30458)) ([fde3f46](https://github.com/angular/angular/commit/fde3f46)) +* **core:** static-query migration fails with default parameter values ([#30269](https://github.com/angular/angular/issues/30269)) ([c3246e6](https://github.com/angular/angular/commit/c3246e6)) +* **core:** static-query migration should gracefully exit if AOT compiler throws ([#30269](https://github.com/angular/angular/issues/30269)) ([a71d8a8](https://github.com/angular/angular/commit/a71d8a8)) +* **core:** static-query migration should handle queries on accessors ([#30327](https://github.com/angular/angular/issues/30327)) ([dd299f9](https://github.com/angular/angular/commit/dd299f9)) +* **core:** static-query migration should not fallback to test strategy ([#30458](https://github.com/angular/angular/issues/30458)) ([0fa48e8](https://github.com/angular/angular/commit/0fa48e8)) +* **core:** static-query migration should not prompt if no queries are used ([#30254](https://github.com/angular/angular/issues/30254)) ([12fb639](https://github.com/angular/angular/commit/12fb639)) +* **core:** static-query usage migration strategy should detect ambiguous query usage ([#30215](https://github.com/angular/angular/issues/30215)) ([e295c6a](https://github.com/angular/angular/commit/e295c6a)) +* **router:** ensure navigations start with the current URL value incase redirect is skipped ([#30344](https://github.com/angular/angular/issues/30344)) ([9b88920](https://github.com/angular/angular/commit/9b88920)), closes [#30340](https://github.com/angular/angular/issues/30340) [#30160](https://github.com/angular/angular/issues/30160) +* **router:** IE 11 bug can break URL unification when comparing objects ([#30393](https://github.com/angular/angular/issues/30393)) ([c383491](https://github.com/angular/angular/commit/c383491)) + + +### Features + +* **bazel:** use rbe_autoconfig() and new container. ([#29336](https://github.com/angular/angular/issues/29336)) ([e562acc](https://github.com/angular/angular/commit/e562acc)) + + + # [8.0.0-rc.3](https://github.com/angular/angular/compare/8.0.0-rc.2...8.0.0-rc.3) (2019-05-07) @@ -52,9 +369,9 @@ ### Bug Fixes * **bazel:** make name param in ng add optional ([#30074](https://github.com/angular/angular/issues/30074)) ([0b5f480](https://github.com/angular/angular/commit/0b5f480)) -* **bazel:** Make sure only single copy of `[@angular](https://github.com/angular)/bazel` is installed ([#30072](https://github.com/angular/angular/issues/30072)) ([2905bf5](https://github.com/angular/angular/commit/2905bf5)) +* **bazel:** Make sure only single copy of `@angular/bazel` is installed ([#30072](https://github.com/angular/angular/issues/30072)) ([2905bf5](https://github.com/angular/angular/commit/2905bf5)) * **bazel:** transitive npm deps in ng_module ([#30065](https://github.com/angular/angular/issues/30065)) ([61365a9](https://github.com/angular/angular/commit/61365a9)) -* **common:** add upgrade sub-package to ng_package rule for [@angular](https://github.com/angular)/common ([#30117](https://github.com/angular/angular/issues/30117)) ([6de4cbd](https://github.com/angular/angular/commit/6de4cbd)), closes [#30055](https://github.com/angular/angular/issues/30055) [#30116](https://github.com/angular/angular/issues/30116) +* **common:** add upgrade sub-package to ng_package rule for @angular/common ([#30117](https://github.com/angular/angular/issues/30117)) ([6de4cbd](https://github.com/angular/angular/commit/6de4cbd)), closes [#30055](https://github.com/angular/angular/issues/30055) [#30116](https://github.com/angular/angular/issues/30116) * **common:** adjust MockPlatformLocation to set state to new object ([#30055](https://github.com/angular/angular/issues/30055)) ([825efa8](https://github.com/angular/angular/commit/825efa8)) * **compiler:** Fix compiler crash due to isSkipSelf of null ([#30075](https://github.com/angular/angular/issues/30075)) ([28fd5ab](https://github.com/angular/angular/commit/28fd5ab)) * **upgrade:** do not break if `onMicrotaskEmpty` emits while a `$digest` is in progress ([#29794](https://github.com/angular/angular/issues/29794)) ([0ddf2e7](https://github.com/angular/angular/commit/0ddf2e7)), closes [#24680](https://github.com/angular/angular/issues/24680) [/github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts#L12](https://github.com//github.com/angular/angular/blob/78146c189/packages/core/src/util/ng_dev_mode.ts/issues/L12) [#24680](https://github.com/angular/angular/issues/24680) @@ -62,7 +379,7 @@ ### Features -* **common:** add [@angular](https://github.com/angular)/common/upgrade package for $location-related APIs ([#30055](https://github.com/angular/angular/issues/30055)) ([152d99e](https://github.com/angular/angular/commit/152d99e)) +* **common:** add @angular/common/upgrade package for $location-related APIs ([#30055](https://github.com/angular/angular/issues/30055)) ([152d99e](https://github.com/angular/angular/commit/152d99e)) * **common:** add ability to retrieve the state from Location service ([#30055](https://github.com/angular/angular/issues/30055)) ([b44b143](https://github.com/angular/angular/commit/b44b143)) * **common:** add ability to track all location changes ([#30055](https://github.com/angular/angular/issues/30055)) ([3a9cf3f](https://github.com/angular/angular/commit/3a9cf3f)) * **common:** add APIs to read component pieces of URL ([#30055](https://github.com/angular/angular/issues/30055)) ([b635fe8](https://github.com/angular/angular/commit/b635fe8)) @@ -125,7 +442,7 @@ ### Bug Fixes -* **bazel:** add configuration_env_vars = ["compile"] to generated [@npm](https://github.com/npm)//[@angular](https://github.com/angular)/bazel/bin:ngc-wrapped nodejs_binary ([#29694](https://github.com/angular/angular/issues/29694)) ([2e66ddf](https://github.com/angular/angular/commit/2e66ddf)) +* **bazel:** add configuration_env_vars = ["compile"] to generated [@npm](https://github.com/npm)//@angular/bazel/bin:ngc-wrapped nodejs_binary ([#29694](https://github.com/angular/angular/issues/29694)) ([2e66ddf](https://github.com/angular/angular/commit/2e66ddf)) * **bazel:** docs formatting ([#29817](https://github.com/angular/angular/issues/29817)) ([cc2e4b6](https://github.com/angular/angular/commit/cc2e4b6)) * **bazel:** remove karma-jasmine from ts_web_test_suite ([#29695](https://github.com/angular/angular/issues/29695)) ([2bd9214](https://github.com/angular/angular/commit/2bd9214)) * **bazel:** support running ng-add on minimal applications ([#29681](https://github.com/angular/angular/issues/29681)) ([9810c6c](https://github.com/angular/angular/commit/9810c6c)), closes [#29680](https://github.com/angular/angular/issues/29680) @@ -200,7 +517,7 @@ let service = TestBed.get(SERVICE_TOKEN); // type Service ### Features -* remove [@angular](https://github.com/angular)/http dependency from [@angular](https://github.com/angular)/platform-server ([#29408](https://github.com/angular/angular/issues/29408)) ([9745f55](https://github.com/angular/angular/commit/9745f55)) +* remove @angular/http dependency from @angular/platform-server ([#29408](https://github.com/angular/angular/issues/29408)) ([9745f55](https://github.com/angular/angular/commit/9745f55)) * **compiler-cli:** ngcc - make logging more configurable ([#29591](https://github.com/angular/angular/issues/29591)) ([8d3d75e](https://github.com/angular/angular/commit/8d3d75e)) * **core:** Add "AbstractType" interface ([#29295](https://github.com/angular/angular/issues/29295)) ([afd4a4e](https://github.com/angular/angular/commit/afd4a4e)), closes [#26491](https://github.com/angular/angular/issues/26491) * **core:** template-var-assignment update schematic ([#29608](https://github.com/angular/angular/issues/29608)) ([7c8f4e3](https://github.com/angular/angular/commit/7c8f4e3)) @@ -260,7 +577,7 @@ This release contains various API docs improvements. * **bazel:** correct regexp test for self-references in metadata ([#29346](https://github.com/angular/angular/issues/29346)) ([9d090cb](https://github.com/angular/angular/commit/9d090cb)) * **bazel:** don't produce self-references in metadata ([#29317](https://github.com/angular/angular/issues/29317)) ([3facdeb](https://github.com/angular/angular/commit/3facdeb)), closes [#29315](https://github.com/angular/angular/issues/29315) * **bazel:** fix strict null checks compile error in packages/bazel/src/schematics/ng-add/index.ts ([#29282](https://github.com/angular/angular/issues/29282)) ([9a7f560](https://github.com/angular/angular/commit/9a7f560)) -* **bazel:** Remove [@angular](https://github.com/angular)/upgrade from dev dependencies ([#29319](https://github.com/angular/angular/issues/29319)) ([1db8bf3](https://github.com/angular/angular/commit/1db8bf3)) +* **bazel:** Remove @angular/upgrade from dev dependencies ([#29319](https://github.com/angular/angular/issues/29319)) ([1db8bf3](https://github.com/angular/angular/commit/1db8bf3)) * **bazel:** Support new e2e project layout ([#29318](https://github.com/angular/angular/issues/29318)) ([8ef690c](https://github.com/angular/angular/commit/8ef690c)) * **bazel:** turn off pure call tree shaking for ng_package ([#29210](https://github.com/angular/angular/issues/29210)) ([4990b93](https://github.com/angular/angular/commit/4990b93)) * **compiler-cli:** incorrect metadata bundle for multiple unnamed re-exports ([#29360](https://github.com/angular/angular/issues/29360)) ([105cfaf](https://github.com/angular/angular/commit/105cfaf)), closes [/github.com/angular/material2/blob/master/tools/package-tools/build-release.ts#L78-L85](https://github.com//github.com/angular/material2/blob/master/tools/package-tools/build-release.ts/issues/L78-L85) @@ -281,8 +598,8 @@ This release contains various API docs improvements. - `` would be wrapped in `` if not inside ``, `` or ``; - `` would be wrapped in `` if not inside ``. -This meachanism of automatic wrapping / auto-correcting was problematic for several reasons: -- it is non-obvious and arbitrary (ex. there are more HTML elements that has rules for parent type); +This mechanism of automatic wrapping / auto-correcting was problematic for several reasons: +- it is non-obvious and arbitrary (ex. there are more HTML elements that have rules for parent type); - it is incorrect for cases where `` / `` are at the root of a component's content, ex.: ```html @@ -683,7 +1000,7 @@ This release contains various API docs improvements. * **bazel:** unable to launch protractor test on windows ([#27850](https://github.com/angular/angular/issues/27850)) ([1e6c9be](https://github.com/angular/angular/commit/1e6c9be)) * **bazel:** devserver entry_module should have underscore name ([#27719](https://github.com/angular/angular/issues/27719)) ([f57916c](https://github.com/angular/angular/commit/f57916c)) * **bazel:** emit full node stack traces when Angular compilation crashes ([#27678](https://github.com/angular/angular/issues/27678)) ([522919a](https://github.com/angular/angular/commit/522919a)) -* **bazel:** fix major/minor semver check between [@angular](https://github.com/angular)/bazel npm packager version and angular bazel repo version ([#27635](https://github.com/angular/angular/issues/27635)) ([1cc08b4](https://github.com/angular/angular/commit/1cc08b4)) +* **bazel:** fix major/minor semver check between @angular/bazel npm packager version and angular bazel repo version ([#27635](https://github.com/angular/angular/issues/27635)) ([1cc08b4](https://github.com/angular/angular/commit/1cc08b4)) * **bazel:** Load http_archive and rules_nodejs dependencies ([#27609](https://github.com/angular/angular/issues/27609)) ([8313ffc](https://github.com/angular/angular/commit/8313ffc)) * **bazel:** ng_package writes unrelevant definitions to bazel out ([#27519](https://github.com/angular/angular/issues/27519)) ([44dfa60](https://github.com/angular/angular/commit/44dfa60)), closes [/github.com/angular/angular/blob/4f9374951d67c75f67a31c110bd61ab72563db7d/packages/bazel/src/ng_package/packager.ts#L105-L124](https://github.com//github.com/angular/angular/blob/4f9374951d67c75f67a31c110bd61ab72563db7d/packages/bazel/src/ng_package/packager.ts/issues/L105-L124) * **bazel:** Set module_name and enable ng test ([#27715](https://github.com/angular/angular/issues/27715)) ([85866de](https://github.com/angular/angular/commit/85866de)) @@ -703,7 +1020,7 @@ This release contains various API docs improvements. * **core:** export a value for InjectFlags ([#27279](https://github.com/angular/angular/issues/27279)) ([23b06af](https://github.com/angular/angular/commit/23b06af)), closes [#27251](https://github.com/angular/angular/issues/27251) * **core:** More precise return type for `InjectableDecorator` ([#27360](https://github.com/angular/angular/issues/27360)) ([4b9948c](https://github.com/angular/angular/commit/4b9948c)), closes [#26942](https://github.com/angular/angular/issues/26942) * **forms:** typed argument for FormBuilder group ([#26985](https://github.com/angular/angular/issues/26985)) ([b0c7561](https://github.com/angular/angular/commit/b0c7561)) -* **platform-server:** add [@angular](https://github.com/angular)/http to the list of peerDependencies ([#27307](https://github.com/angular/angular/issues/27307)) ([32c5be9](https://github.com/angular/angular/commit/32c5be9)), closes [#26154](https://github.com/angular/angular/issues/26154) +* **platform-server:** add @angular/http to the list of peerDependencies ([#27307](https://github.com/angular/angular/issues/27307)) ([32c5be9](https://github.com/angular/angular/commit/32c5be9)), closes [#26154](https://github.com/angular/angular/issues/26154) * **router:** ensure URL is updated after second redirect with UrlUpdateStrategy="eager" ([#27523](https://github.com/angular/angular/issues/27523)) ([ad26cd6](https://github.com/angular/angular/commit/ad26cd6)), closes [#27116](https://github.com/angular/angular/issues/27116) * **router:** update URL after redirects when urlHandlingStrategy='eager' ([#27356](https://github.com/angular/angular/issues/27356)) ([11a8bd8](https://github.com/angular/angular/commit/11a8bd8)), closes [#27076](https://github.com/angular/angular/issues/27076) * **upgrade:** allow nesting components from different downgraded modules ([#27217](https://github.com/angular/angular/issues/27217)) ([bc0ee01](https://github.com/angular/angular/commit/bc0ee01)) @@ -723,7 +1040,7 @@ This release contains various API docs improvements. * **animations:** mark actual descendant node as disabled ([#26180](https://github.com/angular/angular/issues/26180)) ([453589f](https://github.com/angular/angular/commit/453589f)) * **bazel:** devserver entry_module should have underscore name ([#27719](https://github.com/angular/angular/issues/27719)) ([b108e9a](https://github.com/angular/angular/commit/b108e9a)) * **bazel:** emit full node stack traces when Angular compilation crashes ([#27678](https://github.com/angular/angular/issues/27678)) ([0d8528b](https://github.com/angular/angular/commit/0d8528b)) -* **bazel:** fix major/minor semver check between [@angular](https://github.com/angular)/bazel npm packager version and angular bazel repo version ([#27635](https://github.com/angular/angular/issues/27635)) ([3ed1e84](https://github.com/angular/angular/commit/3ed1e84)) +* **bazel:** fix major/minor semver check between @angular/bazel npm packager version and angular bazel repo version ([#27635](https://github.com/angular/angular/issues/27635)) ([3ed1e84](https://github.com/angular/angular/commit/3ed1e84)) * **bazel:** Load http_archive and rules_nodejs dependencies ([#27609](https://github.com/angular/angular/issues/27609)) ([89ace1a](https://github.com/angular/angular/commit/89ace1a)) * **bazel:** ng_package writes unrelevant definitions to bazel out ([#27519](https://github.com/angular/angular/issues/27519)) ([ef056c5](https://github.com/angular/angular/commit/ef056c5)), closes [/github.com/angular/angular/blob/4f9374951d67c75f67a31c110bd61ab72563db7d/packages/bazel/src/ng_package/packager.ts#L105-L124](https://github.com//github.com/angular/angular/blob/4f9374951d67c75f67a31c110bd61ab72563db7d/packages/bazel/src/ng_package/packager.ts/issues/L105-L124) * **bazel:** Read latest versions from latest-versions.ts & use semver check ([#27591](https://github.com/angular/angular/issues/27591)) ([93078e3](https://github.com/angular/angular/commit/93078e3)) @@ -755,7 +1072,7 @@ This release contains various API docs improvements. * **bazel:** ng_package should correctly map to source maps in secondary entry-points ([#27313](https://github.com/angular/angular/issues/27313)) ([fc2c23e](https://github.com/angular/angular/commit/fc2c23e)), closes [#25510](https://github.com/angular/angular/issues/25510) * **compiler-cli:** flatModuleIndex files not generated on windows with multiple input files ([#27200](https://github.com/angular/angular/issues/27200)) ([8087b6b](https://github.com/angular/angular/commit/8087b6b)) * **compiler-cli:** ngtsc shim files not being generated on case-insensitive platforms ([#27466](https://github.com/angular/angular/issues/27466)) ([84f2928](https://github.com/angular/angular/commit/84f2928)), closes [/github.com/Microsoft/TypeScript/blob/3e4c5c95abd515eb9713b881d27ab3a93cc00461/src/compiler/sys.ts#L681-L682](https://github.com//github.com/Microsoft/TypeScript/blob/3e4c5c95abd515eb9713b881d27ab3a93cc00461/src/compiler/sys.ts/issues/L681-L682) -* **platform-server:** add [@angular](https://github.com/angular)/http to the list of peerDependencies ([#27307](https://github.com/angular/angular/issues/27307)) ([236ac06](https://github.com/angular/angular/commit/236ac06)), closes [#26154](https://github.com/angular/angular/issues/26154) +* **platform-server:** add @angular/http to the list of peerDependencies ([#27307](https://github.com/angular/angular/issues/27307)) ([236ac06](https://github.com/angular/angular/commit/236ac06)), closes [#26154](https://github.com/angular/angular/issues/26154) @@ -782,7 +1099,7 @@ This release contains various API docs improvements. * **compiler:** generate inputs with aliases properly ([#26774](https://github.com/angular/angular/issues/26774)) ([19fcfc3](https://github.com/angular/angular/commit/19fcfc3)) * **compiler:** generate relative paths only in summary file errors ([#26759](https://github.com/angular/angular/issues/26759)) ([56f44be](https://github.com/angular/angular/commit/56f44be)) * **core:** ignore comment nodes under unsafe elements ([#25879](https://github.com/angular/angular/issues/25879)) ([d5cbcef](https://github.com/angular/angular/commit/d5cbcef)) -* **core:** Remove static dependency from [@angular](https://github.com/angular)/core to [@angular](https://github.com/angular)/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([d042c4a](https://github.com/angular/angular/commit/d042c4a)) +* **core:** Remove static dependency from @angular/core to @angular/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([d042c4a](https://github.com/angular/angular/commit/d042c4a)) * **core:** support computed base class in metadata inheritance ([#24014](https://github.com/angular/angular/issues/24014)) ([95743e3](https://github.com/angular/angular/commit/95743e3)) * **bazel:** unknown replay compiler error in windows ([#26711](https://github.com/angular/angular/issues/26711)) ([aed95fd](https://github.com/angular/angular/commit/aed95fd)) * **core:** ensure that `ɵdefineNgModule` is available in flat-file formats ([#26403](https://github.com/angular/angular/issues/26403)) ([a64859b](https://github.com/angular/angular/commit/a64859b)) @@ -889,7 +1206,7 @@ This release contains various API docs improvements. * **compiler:** generate inputs with aliases properly ([#26774](https://github.com/angular/angular/issues/26774)) ([19fcfc3](https://github.com/angular/angular/commit/19fcfc3)) * **compiler:** generate relative paths only in summary file errors ([#26759](https://github.com/angular/angular/issues/26759)) ([56f44be](https://github.com/angular/angular/commit/56f44be)) * **core:** ignore comment nodes under unsafe elements ([#25879](https://github.com/angular/angular/issues/25879)) ([d5cbcef](https://github.com/angular/angular/commit/d5cbcef)) -* **core:** Remove static dependency from [@angular](https://github.com/angular)/core to [@angular](https://github.com/angular)/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([d042c4a](https://github.com/angular/angular/commit/d042c4a)) +* **core:** Remove static dependency from @angular/core to @angular/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([d042c4a](https://github.com/angular/angular/commit/d042c4a)) * **core:** support computed base class in metadata inheritance ([#24014](https://github.com/angular/angular/issues/24014)) ([95743e3](https://github.com/angular/angular/commit/95743e3)) @@ -901,7 +1218,7 @@ This release contains various API docs improvements. ### Bug Fixes * **compiler:** generate relative paths only in summary file errors ([#26759](https://github.com/angular/angular/issues/26759)) ([c01f340](https://github.com/angular/angular/commit/c01f340)) -* **core:** Remove static dependency from [@angular](https://github.com/angular)/core to [@angular](https://github.com/angular)/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([#26879](https://github.com/angular/angular/issues/26879)) ([257ac83](https://github.com/angular/angular/commit/257ac83)) +* **core:** Remove static dependency from @angular/core to @angular/compiler ([#26734](https://github.com/angular/angular/issues/26734)) ([#26879](https://github.com/angular/angular/issues/26879)) ([257ac83](https://github.com/angular/angular/commit/257ac83)) * **core:** support computed base class in metadata inheritance ([#24014](https://github.com/angular/angular/issues/24014)) ([b3c6409](https://github.com/angular/angular/commit/b3c6409)) @@ -978,7 +1295,7 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **compiler:** update compiler to flatten nested template fns ([#24943](https://github.com/angular/angular/issues/24943)) ([fe14f18](https://github.com/angular/angular/commit/fe14f18)) * **compiler:** update compiler to generate new slot allocations ([#25607](https://github.com/angular/angular/issues/25607)) ([27e2039](https://github.com/angular/angular/commit/27e2039)) * **core:** In Testability.whenStable update callback, pass more complete ([#25010](https://github.com/angular/angular/issues/25010)) ([16c03c0](https://github.com/angular/angular/commit/16c03c0)) -* **core:** add missing `peerDependency ` to `[@angular](https://github.com/angular)/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54) +* **core:** add missing `peerDependency ` to `@angular/compiler` ([#26033](https://github.com/angular/angular/issues/26033)) ([549de1e](https://github.com/angular/angular/commit/549de1e)), closes [/github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e#diff-58563046c4439699f2e6a89187099a54](https://github.com//github.com/angular/angular/commit/919f42fea1df4b9e38b7d688aef5f2de668e9d3e/issues/diff-58563046c4439699f2e6a89187099a54) * **core:** allow null value for renderer setElement(…) ([#17065](https://github.com/angular/angular/issues/17065)) ([ff15043](https://github.com/angular/angular/commit/ff15043)), closes [#13686](https://github.com/angular/angular/issues/13686) * **core:** do not clear element content when using shadow dom ([#24861](https://github.com/angular/angular/issues/24861)) ([6e828bb](https://github.com/angular/angular/commit/6e828bb)) * **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([1f59f2f](https://github.com/angular/angular/commit/1f59f2f)) @@ -1023,7 +1340,6 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **bazel:** protractor rule should include *.e2e-spec.js ([#25701](https://github.com/angular/angular/issues/25701)) ([ed6b68b](https://github.com/angular/angular/commit/ed6b68b)) * **core:** size regression with closure compiler ([#25531](https://github.com/angular/angular/issues/25531)) ([ebcf762](https://github.com/angular/angular/commit/ebcf762)) -* **docs-infra:** show "suggest edits" only for /guide and /tutorial dirs ([#24378](https://github.com/angular/angular/issues/24378)) ([66b7870](https://github.com/angular/angular/commit/66b7870)) * **upgrade:** trigger `$destroy` event on upgraded component element ([#25357](https://github.com/angular/angular/issues/25357)) ([82e0676](https://github.com/angular/angular/commit/82e0676)), closes [#25334](https://github.com/angular/angular/issues/25334) * **router:** warn if navigation triggered outside Angular zone ([#24959](https://github.com/angular/angular/issues/24959)) ([23a96dc](https://github.com/angular/angular/commit/23a96dc)), closes [#15770](https://github.com/angular/angular/issues/15770) [#15946](https://github.com/angular/angular/issues/15946) [#24728](https://github.com/angular/angular/issues/24728) @@ -1115,7 +1431,6 @@ Note: the 6.1.5 release on npm accidentally glitched-out midway, so we cut 6.1.6 * **core:** Injector correctly honors the @Self flag ([#24520](https://github.com/angular/angular/issues/24520)) ([ccbda9d](https://github.com/angular/angular/commit/ccbda9d)) * **core:** avoid eager providers re-initialization ([#23559](https://github.com/angular/angular/issues/23559)) ([0c6dc45](https://github.com/angular/angular/commit/0c6dc45)) * **core:** call ngOnDestroy on all services that have it ([#23755](https://github.com/angular/angular/issues/23755)) ([fc03427](https://github.com/angular/angular/commit/fc03427)), closes [#22466](https://github.com/angular/angular/issues/22466) [#22240](https://github.com/angular/angular/issues/22240) [#14818](https://github.com/angular/angular/issues/14818) -* **docs-infra:** fix table header layout in API pages ([#24919](https://github.com/angular/angular/issues/24919)) ([3cd9645](https://github.com/angular/angular/commit/3cd9645)) * **elements:** always check to create strategy ([#23825](https://github.com/angular/angular/issues/23825)) ([b1cda36](https://github.com/angular/angular/commit/b1cda36)) * **elements:** prevent closure renaming of platform properties ([#23843](https://github.com/angular/angular/issues/23843)) ([d4b8b24](https://github.com/angular/angular/commit/d4b8b24)) * **forms:** properly handle special properties in FormGroup.get ([#22249](https://github.com/angular/angular/issues/22249)) ([9367e91](https://github.com/angular/angular/commit/9367e91)), closes [#17195](https://github.com/angular/angular/issues/17195) @@ -1232,7 +1547,6 @@ For example: * **animations:** always render end-state styles for orphaned DOM nodes ([#24236](https://github.com/angular/angular/issues/24236)) ([0139173](https://github.com/angular/angular/commit/0139173)) * **bazel:** Allow ng_module to depend on targets w no deps ([#24446](https://github.com/angular/angular/issues/24446)) ([ea3669e](https://github.com/angular/angular/commit/ea3669e)) -* **docs-infra:** use script nomodule to load IE polyfills, skip other polyfills ([#24317](https://github.com/angular/angular/issues/24317)) ([e876535](https://github.com/angular/angular/commit/e876535)), closes [#23647](https://github.com/angular/angular/issues/23647) * **router:** fix lazy loading of aux routes ([#23459](https://github.com/angular/angular/issues/23459)) ([d20877b](https://github.com/angular/angular/commit/d20877b)), closes [#10981](https://github.com/angular/angular/issues/10981) * **service-worker:** fix `SwPush.unsubscribe()` ([#24162](https://github.com/angular/angular/issues/24162)) ([ea2987c](https://github.com/angular/angular/commit/ea2987c)), closes [#24095](https://github.com/angular/angular/issues/24095) @@ -1427,7 +1741,7 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **upgrade:** propagate return value of resumeBootstrap ([#22754](https://github.com/angular/angular/issues/22754)) ([a2330ff](https://github.com/angular/angular/commit/a2330ff)), closes [#22723](https://github.com/angular/angular/issues/22723) * **upgrade:** two-way binding and listening for event ([#22772](https://github.com/angular/angular/issues/22772)) ([2b3de63](https://github.com/angular/angular/commit/2b3de63)), closes [#22734](https://github.com/angular/angular/issues/22734) * **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([8a85888](https://github.com/angular/angular/commit/8a85888)), closes [#22392](https://github.com/angular/angular/issues/22392) -* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([f089bf5](https://github.com/angular/angular/commit/f089bf5)) +* **upgrade:** correctly handle `=` bindings in `@angular/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([f089bf5](https://github.com/angular/angular/commit/f089bf5)) * **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([13ab91e](https://github.com/angular/angular/commit/13ab91e)), closes [#22175](https://github.com/angular/angular/issues/22175) @@ -1562,7 +1876,7 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **router:** don't mutate route configs ([#22358](https://github.com/angular/angular/issues/22358)) ([8f0a064](https://github.com/angular/angular/commit/8f0a064)), closes [#22203](https://github.com/angular/angular/issues/22203) * **router:** fix URL serialization so special characters are only encoded where needed ([#22337](https://github.com/angular/angular/issues/22337)) ([789a47e](https://github.com/angular/angular/commit/789a47e)), closes [#10280](https://github.com/angular/angular/issues/10280) * **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([4aef9de](https://github.com/angular/angular/commit/4aef9de)), closes [#22392](https://github.com/angular/angular/issues/22392) -* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390)) +* **upgrade:** correctly handle `=` bindings in `@angular/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390)) * **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([a9a0e27](https://github.com/angular/angular/commit/a9a0e27)), closes [#22175](https://github.com/angular/angular/issues/22175) @@ -1576,7 +1890,7 @@ To learn about the release highlights and our new CLI-powered update workflow fo * **platform-server:** generate correct stylings for camel case names ([#22263](https://github.com/angular/angular/issues/22263)) ([de02a7a](https://github.com/angular/angular/commit/de02a7a)), closes [#19235](https://github.com/angular/angular/issues/19235) * **router:** don't mutate route configs ([#22358](https://github.com/angular/angular/issues/22358)) ([8f0a064](https://github.com/angular/angular/commit/8f0a064)), closes [#22203](https://github.com/angular/angular/issues/22203) * **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([4aef9de](https://github.com/angular/angular/commit/4aef9de)), closes [#22392](https://github.com/angular/angular/issues/22392) -* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390)) +* **upgrade:** correctly handle `=` bindings in `@angular/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390)) * **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([a9a0e27](https://github.com/angular/angular/commit/a9a0e27)), closes [#22175](https://github.com/angular/angular/issues/22175) @@ -2023,7 +2337,7 @@ Note: Due to an animation fix back in 5.1.1 ([c2b3792](https://github.com/angula * **platform-server:** provide a way to hook into renderModule* ([#19023](https://github.com/angular/angular/issues/19023)) ([8dfc3c3](https://github.com/angular/angular/commit/8dfc3c3)) * **router:** add ActivationStart/End events ([8f79150](https://github.com/angular/angular/commit/8f79150)) * **router:** add events tracking activation of individual routes ([49cd851](https://github.com/angular/angular/commit/49cd851)) -* **service-worker:** introduce the [@angular](https://github.com/angular)/service-worker package ([#19274](https://github.com/angular/angular/issues/19274)) ([d442b68](https://github.com/angular/angular/commit/d442b68)) +* **service-worker:** introduce the @angular/service-worker package ([#19274](https://github.com/angular/angular/issues/19274)) ([d442b68](https://github.com/angular/angular/commit/d442b68)) * **upgrade:** propagate touched state of NgModelController ([59c23c7](https://github.com/angular/angular/commit/59c23c7)) * **upgrade:** support lazy-loading Angular module into AngularJS app ([30e76fc](https://github.com/angular/angular/commit/30e76fc)) * update angular to support TypeScript 2.4 ([ca5aeba](https://github.com/angular/angular/commit/ca5aeba)) @@ -2247,7 +2561,7 @@ Because of multiple bugs and browser inconsistencies, we have dropped the intl a ### Bug Fixes * **animations:** do not leak DOM nodes/styling for host triggered animations ([#18853](https://github.com/angular/angular/issues/18853)) ([1cc3fe2](https://github.com/angular/angular/commit/1cc3fe2)), closes [#18606](https://github.com/angular/angular/issues/18606) -* **common:** fix improper packaging for [@angular](https://github.com/angular)/common/http ([#18613](https://github.com/angular/angular/issues/18613)) ([a203a95](https://github.com/angular/angular/commit/a203a95)) +* **common:** fix improper packaging for @angular/common/http ([#18613](https://github.com/angular/angular/issues/18613)) ([a203a95](https://github.com/angular/angular/commit/a203a95)) * **common:** fix XSSI prefix stripping by using JSON.parse always ([#18466](https://github.com/angular/angular/issues/18466)) ([8821723](https://github.com/angular/angular/commit/8821723)), closes [#18396](https://github.com/angular/angular/issues/18396) [#18453](https://github.com/angular/angular/issues/18453) * **compiler:** normalize the locale name ([#18963](https://github.com/angular/angular/issues/18963)) ([497e017](https://github.com/angular/angular/commit/497e017)) * **core:** complete EventEmitter in QueryList on component destroy ([#18902](https://github.com/angular/angular/issues/18902)) ([7d137d7](https://github.com/angular/angular/commit/7d137d7)), closes [#18741](https://github.com/angular/angular/issues/18741) @@ -2588,7 +2902,7 @@ Note: the 4.4.0 release on npm accidentally glitched-out midway, so we cut 4.4.1 * **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059)) * **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([f0f6544](https://github.com/angular/angular/commit/f0f6544)), closes [#15578](https://github.com/angular/angular/issues/15578) * **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([fcc91d8](https://github.com/angular/angular/commit/fcc91d8)), closes [#14321](https://github.com/angular/angular/issues/14321) -* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9da6340](https://github.com/angular/angular/commit/9da6340)), closes [#15521](https://github.com/angular/angular/issues/15521) +* **http:** flatten metadata for @angular/http/testing ([9da6340](https://github.com/angular/angular/commit/9da6340)), closes [#15521](https://github.com/angular/angular/issues/15521) * **http:** honor RequestArgs.search and RequestArgs.params map type ([aef5245](https://github.com/angular/angular/commit/aef5245)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392) * **http:** introduce encodingHint for text() for better ArrayBuffer support ([7ae7a84](https://github.com/angular/angular/commit/7ae7a84)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420) * **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([5d4b36f](https://github.com/angular/angular/commit/5d4b36f)), closes [#16310](https://github.com/angular/angular/issues/16310) @@ -2644,7 +2958,7 @@ Note: the 4.4.0 release on npm accidentally glitched-out midway, so we cut 4.4.1 * **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([d0e1688](https://github.com/angular/angular/commit/d0e1688)) * **core:** detach projected views when a parent view is destroyed ([#16592](https://github.com/angular/angular/issues/16592)) ([ee6705a](https://github.com/angular/angular/commit/ee6705a)), closes [#15578](https://github.com/angular/angular/issues/15578) * **core:** projected views should be dirty checked when the declaring component is dirty checked. ([#16592](https://github.com/angular/angular/issues/16592)) ([9218812](https://github.com/angular/angular/commit/9218812)), closes [#14321](https://github.com/angular/angular/issues/14321) -* **http:** flatten metadata for [@angular](https://github.com/angular)/http/testing ([9c70a3c](https://github.com/angular/angular/commit/9c70a3c)), closes [#15521](https://github.com/angular/angular/issues/15521) +* **http:** flatten metadata for @angular/http/testing ([9c70a3c](https://github.com/angular/angular/commit/9c70a3c)), closes [#15521](https://github.com/angular/angular/issues/15521) * **http:** honor RequestArgs.search and RequestArgs.params map type ([63066f7](https://github.com/angular/angular/commit/63066f7)), closes [#15761](https://github.com/angular/angular/issues/15761) [#16392](https://github.com/angular/angular/issues/16392) * **http:** introduce encodingHint for text() for better ArrayBuffer support ([ec3b6e9](https://github.com/angular/angular/commit/ec3b6e9)), closes [#15932](https://github.com/angular/angular/issues/15932) [#16420](https://github.com/angular/angular/issues/16420) * **router:** fix redirect to a URL with a param having multiple values ([#16376](https://github.com/angular/angular/issues/16376)) ([915eae5](https://github.com/angular/angular/commit/915eae5)), closes [#16310](https://github.com/angular/angular/issues/16310) @@ -3274,7 +3588,7 @@ Note: the 4.0.0-rc.0 release on npm accidentally omitted one bug fix, so we cut * **core:** add isStable Observable property to ApplicationRef to indicate when it's stable and unstable ([#14337](https://github.com/angular/angular/issues/14337)) ([c481798](https://github.com/angular/angular/commit/c481798)) * **platform-server:** add API to render Module and ModuleFactory to string ([#14381](https://github.com/angular/angular/issues/14381)) ([b4d444a](https://github.com/angular/angular/commit/b4d444a)) * **platform-server:** Implement PlatformLocation for platformServer() ([#14405](https://github.com/angular/angular/issues/14405)) ([9e28568](https://github.com/angular/angular/commit/9e28568)) -* **platform-server:** support [@angular](https://github.com/angular)/http from [@angular](https://github.com/angular)/platform-server ([9559d3e](https://github.com/angular/angular/commit/9559d3e)) +* **platform-server:** support @angular/http from @angular/platform-server ([9559d3e](https://github.com/angular/angular/commit/9559d3e)) * **tsc-wrapped:** add an option to `ngc` to bundle metadata ([#14509](https://github.com/angular/angular/issues/14509)) ([3b89670](https://github.com/angular/angular/commit/3b89670)) @@ -4235,7 +4549,7 @@ Note: The 2.2.0-beta.0 release also contains all the changes present in the 2.1. * **compiler:** properly shim `:host:before` and `:host(:before)` ([#12171](https://github.com/angular/angular/issues/12171)) ([aa92512](https://github.com/angular/angular/commit/aa92512)), closes [#12165](https://github.com/angular/angular/issues/12165) * **compiler:** validate `@HostBinding` name ([#12139](https://github.com/angular/angular/issues/12139)) ([13ecc14](https://github.com/angular/angular/commit/13ecc14)) * **compiler-cli:** don't clone static symbols when simplifying annotation metadata ([#12158](https://github.com/angular/angular/issues/12158)) ([8c477b2](https://github.com/angular/angular/commit/8c477b2)) -* **compiler-cli:** remove peerDependency on [@angular](https://github.com/angular)/platform-server ([#12122](https://github.com/angular/angular/issues/12122)) ([71b7654](https://github.com/angular/angular/commit/71b7654)) +* **compiler-cli:** remove peerDependency on @angular/platform-server ([#12122](https://github.com/angular/angular/issues/12122)) ([71b7654](https://github.com/angular/angular/commit/71b7654)) * **compiler-cli:** remove unused parse5 dependency from package.json ([eaaec69](https://github.com/angular/angular/commit/eaaec69)) * **forms:** allow optional fields with pattern and minlength validators ([#12147](https://github.com/angular/angular/issues/12147)) ([d22eeb7](https://github.com/angular/angular/commit/d22eeb7)) * **forms:** properly validate blank strings with minlength ([#12091](https://github.com/angular/angular/issues/12091)) ([f50c1da](https://github.com/angular/angular/commit/f50c1da)) diff --git a/README.md b/README.md index 6693a4b272..0ad87d18a4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,6 @@ guidelines for [contributing][contributing] and then check out one of our issues [browserstack]: https://www.browserstack.com/automate/public-build/LzF3RzBVVGt6VWE2S0hHaC9uYllOZz09LS1BVjNTclBKV0x4eVRlcjA4QVY1M0N3PT0=--eb4ce8c8dc2c1c5b2b5352d473ee12a73ac20e06 [contributing]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md -[quickstart]: https://angular.io/guide/quickstart +[quickstart]: https://angular.io/start [changelog]: https://github.com/angular/angular/blob/master/CHANGELOG.md [ng]: https://angular.io diff --git a/WORKSPACE b/WORKSPACE index 4a1fe3b291..912617f7d3 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,8 +15,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch rules_nodejs so we can install our npm dependencies http_archive( name = "build_bazel_rules_nodejs", - sha256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.12/rules_nodejs-0.27.12.tar.gz"], + sha256 = "395b7568f20822c13fc5abc65b1eced637446389181fda3a108fdd6ff2cac1e9", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.29.2/rules_nodejs-0.29.2.tar.gz"], ) # Check the bazel version and download npm dependencies @@ -96,7 +96,7 @@ web_test_repositories() # Temporary work-around for https://github.com/angular/angular/issues/28681 # TODO(gregmagolan): go back to @io_bazel_rules_webtesting browser_repositories -load("@npm_bazel_karma//:browser_repositories.bzl", "browser_repositories") +load("//:browser_repositories.bzl", "browser_repositories") browser_repositories() diff --git a/aio/content/examples/.gitignore b/aio/content/examples/.gitignore index 905ed736fe..22dcf835a8 100644 --- a/aio/content/examples/.gitignore +++ b/aio/content/examples/.gitignore @@ -23,6 +23,9 @@ **/bs-config.e2e.json **/bs-config.json **/package.json +**/tsconfig.json +**/tsconfig.app.json +**/tsconfig.spec.json **/tslint.json **/karma-test-shim.js **/browser-test-shim.js diff --git a/aio/content/examples/ajs-quick-reference/e2e/src/app.e2e-spec.ts b/aio/content/examples/ajs-quick-reference/e2e/src/app.e2e-spec.ts index e27f56e68b..0eef85ffde 100644 --- a/aio/content/examples/ajs-quick-reference/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/ajs-quick-reference/e2e/src/app.e2e-spec.ts @@ -67,8 +67,8 @@ describe('AngularJS to Angular Quick Reference Tests', function () { testFavoriteHero('Magneta', 'No movie, sorry!'); }); - it('should display a movie for Mr. Nice', function () { - testFavoriteHero('Mr. Nice', 'Excellent choice!'); + it('should display a movie for Dr Nice', function () { + testFavoriteHero('Dr Nice', 'Excellent choice!'); }); function testImagesAreDisplayed(isDisplayed: boolean) { diff --git a/aio/content/examples/ajs-quick-reference/src/app/movie.service.ts b/aio/content/examples/ajs-quick-reference/src/app/movie.service.ts index 887774d570..f74e606544 100644 --- a/aio/content/examples/ajs-quick-reference/src/app/movie.service.ts +++ b/aio/content/examples/ajs-quick-reference/src/app/movie.service.ts @@ -18,12 +18,12 @@ export class MovieService { approvalRating: .97 }, { - hero: 'Mr. Nice', + hero: 'Dr Nice', imageurl: 'assets/images/villain.png', movieId: 2, mpaa: 'pg-13', releaseDate: '2015-12-18T00:00:00', - title: 'No More Mr. Nice Guy', + title: 'No More Dr Nice', price: 14.95, starRating: 4.6, approvalRating: .94 diff --git a/aio/content/examples/animations/src/app/mock-heroes.ts b/aio/content/examples/animations/src/app/mock-heroes.ts index 1771a7103b..cd3506f574 100644 --- a/aio/content/examples/animations/src/app/mock-heroes.ts +++ b/aio/content/examples/animations/src/app/mock-heroes.ts @@ -2,7 +2,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/architecture/e2e/src/app.e2e-spec.ts b/aio/content/examples/architecture/e2e/src/app.e2e-spec.ts index 8ad5c9ab74..6dbc6952b8 100644 --- a/aio/content/examples/architecture/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/architecture/e2e/src/app.e2e-spec.ts @@ -31,7 +31,7 @@ describe('Architecture', () => { function heroTests() { - const targetHero: Hero = { id: 2, name: 'Mr. Nice' }; + const targetHero: Hero = { id: 2, name: 'Dr Nice' }; it('has the right number of heroes', () => { let page = getPageElts(); diff --git a/aio/content/examples/architecture/src/app/backend.service.ts b/aio/content/examples/architecture/src/app/backend.service.ts index c05a8fcb8a..145be4cad5 100644 --- a/aio/content/examples/architecture/src/app/backend.service.ts +++ b/aio/content/examples/architecture/src/app/backend.service.ts @@ -5,7 +5,7 @@ import { Hero } from './hero'; const HEROES = [ new Hero('Windstorm', 'Weather mastery'), - new Hero('Mr. Nice', 'Killing them with kindness'), + new Hero('Dr Nice', 'Killing them with kindness'), new Hero('Magneta', 'Manipulates metallic objects') ]; diff --git a/aio/content/examples/architecture/src/app/mini-app.ts b/aio/content/examples/architecture/src/app/mini-app.ts index b064428be3..85689b6304 100644 --- a/aio/content/examples/architecture/src/app/mini-app.ts +++ b/aio/content/examples/architecture/src/app/mini-app.ts @@ -12,7 +12,7 @@ import { Component } from '@angular/core'; // #enddocregion import-core-component @Component({ - selector: 'my-app', + selector: 'app-root', template: 'Welcome to Angular' }) export class AppComponent { diff --git a/aio/content/examples/bootstrapping/e2e/src/app.e2e-spec.ts b/aio/content/examples/bootstrapping/e2e/src/app.e2e-spec.ts index cd4922ccd3..676d41469c 100644 --- a/aio/content/examples/bootstrapping/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/bootstrapping/e2e/src/app.e2e-spec.ts @@ -9,6 +9,6 @@ describe('feature-modules App', () => { it('should display message saying app works', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('app works!'); + expect(page.getTitleText()).toEqual('app works!'); }); }); diff --git a/aio/content/examples/cli-quickstart/bs-config.cli.json b/aio/content/examples/cli-quickstart/bs-config.cli.json deleted file mode 100644 index ac61d35f83..0000000000 --- a/aio/content/examples/cli-quickstart/bs-config.cli.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "open": false, - "logLevel": "silent", - "port": 8080, - "server": { - "baseDir": "dist", - "middleware": { - "0": null - } - } -} diff --git a/aio/content/examples/cli-quickstart/e2e/src/app.e2e-spec.ts b/aio/content/examples/cli-quickstart/e2e/src/app.e2e-spec.ts deleted file mode 100644 index 3f0760b56e..0000000000 --- a/aio/content/examples/cli-quickstart/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; // necessary for es6 output in node - -import { browser, element, by } from 'protractor'; - -describe('cli-quickstart App', () => { - beforeEach(() => { - return browser.get('/'); - }); - - it('should display message saying app works', () => { - let pageTitle = element(by.css('app-root h1')).getText(); - expect(pageTitle).toEqual('Welcome to My First Angular App!!'); - }); -}); diff --git a/aio/content/examples/cli-quickstart/example-config.json b/aio/content/examples/cli-quickstart/example-config.json deleted file mode 100644 index 313764c3c6..0000000000 --- a/aio/content/examples/cli-quickstart/example-config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "build": "build:cli", - "run": "serve:cli" -} diff --git a/aio/content/examples/cli-quickstart/src/app/app.component.css b/aio/content/examples/cli-quickstart/src/app/app.component.css deleted file mode 100644 index a2b21fae82..0000000000 --- a/aio/content/examples/cli-quickstart/src/app/app.component.css +++ /dev/null @@ -1,6 +0,0 @@ -/* #docregion */ -h1 { - color: #369; - font-family: Arial, Helvetica, sans-serif; - font-size: 250%; -} diff --git a/aio/content/examples/cli-quickstart/src/app/app.component.html b/aio/content/examples/cli-quickstart/src/app/app.component.html deleted file mode 100644 index 24c56edee4..0000000000 --- a/aio/content/examples/cli-quickstart/src/app/app.component.html +++ /dev/null @@ -1,19 +0,0 @@ - -
-

- Welcome to {{ title }}! -

- Angular Logo -
-

Here are some links to help you start:

- diff --git a/aio/content/examples/cli-quickstart/src/app/app.component.spec.ts b/aio/content/examples/cli-quickstart/src/app/app.component.spec.ts deleted file mode 100644 index 1f5da50d19..0000000000 --- a/aio/content/examples/cli-quickstart/src/app/app.component.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TestBed, async } from '@angular/core/testing'; -import { AppComponent } from './app.component'; -describe('AppComponent', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], - }).compileComponents(); - })); - - it('should create the app', async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - - it(`should have as title 'app'`, async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app.title).toMatch(/app/i); - })); - - it('should render title in a h1 tag', async(() => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toMatch(/app/i); - })); -}); diff --git a/aio/content/examples/cli-quickstart/src/app/app.component.ts b/aio/content/examples/cli-quickstart/src/app/app.component.ts deleted file mode 100644 index 1c462d321b..0000000000 --- a/aio/content/examples/cli-quickstart/src/app/app.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -// #docregion import -import { Component } from '@angular/core'; -// #enddocregion import - -// #docregion metadata, component -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] -}) -// #enddocregion metadata -// #docregion title, class -export class AppComponent { - title = 'My First Angular App!'; -} -// #enddocregion title, class, component diff --git a/aio/content/examples/cli-quickstart/src/app/app.module.ts b/aio/content/examples/cli-quickstart/src/app/app.module.ts deleted file mode 100644 index f65716351a..0000000000 --- a/aio/content/examples/cli-quickstart/src/app/app.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; - -import { AppComponent } from './app.component'; - -@NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule - ], - providers: [], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/aio/content/examples/cli-quickstart/zipper.json b/aio/content/examples/cli-quickstart/zipper.json deleted file mode 100644 index 1deb27d7fb..0000000000 --- a/aio/content/examples/cli-quickstart/zipper.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files":[ - "!**/*.d.ts", - "!**/*.js", - "!**/*.[0-9].*", - "angular.json", - "protractor.conf.js" - ] -} diff --git a/aio/content/examples/component-interaction/e2e/src/app.e2e-spec.ts b/aio/content/examples/component-interaction/e2e/src/app.e2e-spec.ts index 7637a6affc..abc8ecf765 100644 --- a/aio/content/examples/component-interaction/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/component-interaction/e2e/src/app.e2e-spec.ts @@ -13,7 +13,7 @@ describe('Component Communication Cookbook Tests', function () { describe('Parent-to-child communication', function() { // #docregion parent-to-child // ... - let _heroNames = ['Mr. IQ', 'Magneta', 'Bombasto']; + let _heroNames = ['Dr IQ', 'Magneta', 'Bombasto']; let _masterName = 'Master'; it('should pass properties to children properly', function () { @@ -36,7 +36,7 @@ describe('Component Communication Cookbook Tests', function () { // ... it('should display trimmed, non-empty names', function () { let _nonEmptyNameIndex = 0; - let _nonEmptyName = '"Mr. IQ"'; + let _nonEmptyName = '"Dr IQ"'; let parent = element.all(by.tagName('app-name-parent')).get(0); let hero = parent.all(by.tagName('app-name-child')).get(_nonEmptyNameIndex); diff --git a/aio/content/examples/component-interaction/src/app/countdown-parent.component.ts b/aio/content/examples/component-interaction/src/app/countdown-parent.component.ts index 9f4e5bd4df..57f7230d89 100644 --- a/aio/content/examples/component-interaction/src/app/countdown-parent.component.ts +++ b/aio/content/examples/component-interaction/src/app/countdown-parent.component.ts @@ -39,7 +39,7 @@ export class CountdownLocalVarParentComponent { } }) export class CountdownViewChildParentComponent implements AfterViewInit { - @ViewChild(CountdownTimerComponent) + @ViewChild(CountdownTimerComponent, {static: false}) private timerComponent: CountdownTimerComponent; seconds() { return 0; } diff --git a/aio/content/examples/component-interaction/src/app/hero.ts b/aio/content/examples/component-interaction/src/app/hero.ts index a7b70f48e8..3e80ad51c2 100644 --- a/aio/content/examples/component-interaction/src/app/hero.ts +++ b/aio/content/examples/component-interaction/src/app/hero.ts @@ -3,7 +3,7 @@ export class Hero { } export const HEROES = [ - {name: 'Mr. IQ'}, + {name: 'Dr IQ'}, {name: 'Magneta'}, {name: 'Bombasto'} ]; diff --git a/aio/content/examples/component-interaction/src/app/name-parent.component.ts b/aio/content/examples/component-interaction/src/app/name-parent.component.ts index 2c8c23b781..747fbea166 100644 --- a/aio/content/examples/component-interaction/src/app/name-parent.component.ts +++ b/aio/content/examples/component-interaction/src/app/name-parent.component.ts @@ -9,7 +9,7 @@ import { Component } from '@angular/core'; ` }) export class NameParentComponent { - // Displays 'Mr. IQ', '', 'Bombasto' - names = ['Mr. IQ', ' ', ' Bombasto ']; + // Displays 'Dr IQ', '', 'Bombasto' + names = ['Dr IQ', ' ', ' Bombasto ']; } // #enddocregion diff --git a/aio/content/examples/component-interaction/src/app/votetaker.component.ts b/aio/content/examples/component-interaction/src/app/votetaker.component.ts index d553af0d7b..7b3de1059b 100644 --- a/aio/content/examples/component-interaction/src/app/votetaker.component.ts +++ b/aio/content/examples/component-interaction/src/app/votetaker.component.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; export class VoteTakerComponent { agreed = 0; disagreed = 0; - voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto']; + voters = ['Narco', 'Celeritas', 'Bombasto']; onVoted(agreed: boolean) { agreed ? this.agreed++ : this.disagreed++; diff --git a/aio/content/examples/dependency-injection-in-action/e2e/src/app.e2e-spec.ts b/aio/content/examples/dependency-injection-in-action/e2e/src/app.e2e-spec.ts index 00dd11868c..4d611f5698 100644 --- a/aio/content/examples/dependency-injection-in-action/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/dependency-injection-in-action/e2e/src/app.e2e-spec.ts @@ -23,8 +23,8 @@ describe('Dependency Injection Cookbook', function () { expect(sortedHeroes).toBeDefined(); }); - it('Mr. Nice should be in sorted heroes', function () { - let sortedHero = element.all(by.xpath('//sorted-heroes/[text()="Mr. Nice" and position()=2]')).get(0); + it('Dr Nice should be in sorted heroes', function () { + let sortedHero = element.all(by.xpath('//sorted-heroes/[text()="Dr Nice" and position()=2]')).get(0); expect(sortedHero).toBeDefined(); }); @@ -60,7 +60,7 @@ describe('Dependency Injection Cookbook', function () { it('should render Hero-of-the-Month runner-ups', function () { let runnersUp = element(by.id('rups1')).getText(); - expect(runnersUp).toContain('RubberMan, Mr. Nice'); + expect(runnersUp).toContain('RubberMan, Dr Nice'); }); it('should render DateLogger log entry in Hero-of-the-Month', function () { diff --git a/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts b/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts index 44db98ee7a..369e2be532 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/hero.service.ts @@ -11,7 +11,7 @@ export class HeroService { private heroes: Array = [ new Hero(1, 'RubberMan', 'Hero of many talents', '123-456-7899'), new Hero(2, 'Magma', 'Hero of all trades', '555-555-5555'), - new Hero(3, 'Mr. Nice', 'The name says it all', '111-222-3333') + new Hero(3, 'Dr Nice', 'The name says it all', '111-222-3333') ]; getHeroById(id: number): Hero { diff --git a/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts b/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts index d0fb0ca54e..4dc074fd8f 100644 --- a/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts +++ b/aio/content/examples/dependency-injection-in-action/src/app/storage.component.ts @@ -29,10 +29,10 @@ export class StorageComponent implements OnInit { } setSession() { - this.sessionStorageService.set('hero', 'Mr. Nice - Session'); + this.sessionStorageService.set('hero', 'Dr Nice - Session'); } setLocal() { - this.localStorageService.set('hero', 'Mr. Nice - Local'); + this.localStorageService.set('hero', 'Dr Nice - Local'); } } diff --git a/aio/content/examples/dependency-injection/e2e/src/app.e2e-spec.ts b/aio/content/examples/dependency-injection/e2e/src/app.e2e-spec.ts index 12fe6ae474..00bf3a9d0a 100644 --- a/aio/content/examples/dependency-injection/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/dependency-injection/e2e/src/app.e2e-spec.ts @@ -56,7 +56,7 @@ describe('Dependency Injection Tests', function () { }); it('Hero displays as expected', function () { - expectedMsg = 'Mr. Nice'; + expectedMsg = 'Dr Nice'; expect(element(by.css('#hero')).getText()).toEqual(expectedMsg); }); diff --git a/aio/content/examples/dependency-injection/src/app/heroes/mock-heroes.ts b/aio/content/examples/dependency-injection/src/app/heroes/mock-heroes.ts index a836faa1bd..61122b9fb2 100644 --- a/aio/content/examples/dependency-injection/src/app/heroes/mock-heroes.ts +++ b/aio/content/examples/dependency-injection/src/app/heroes/mock-heroes.ts @@ -2,7 +2,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, isSecret: false, name: 'Mr. Nice' }, + { id: 11, isSecret: false, name: 'Dr Nice' }, { id: 12, isSecret: false, name: 'Narco' }, { id: 13, isSecret: false, name: 'Bombasto' }, { id: 14, isSecret: false, name: 'Celeritas' }, diff --git a/aio/content/examples/displaying-data/src/app/app-ctor.component.1.ts b/aio/content/examples/displaying-data/src/app/app-ctor.component.1.ts index 5fd9d45073..aa696a3d63 100644 --- a/aio/content/examples/displaying-data/src/app/app-ctor.component.1.ts +++ b/aio/content/examples/displaying-data/src/app/app-ctor.component.1.ts @@ -1,14 +1,14 @@ import { Component } from '@angular/core'; @Component({ - selector: 'app-ctor', + selector: 'app-root', template: `

{{title}} [Ctor version]

My favorite hero is: {{myHero}}

` }) // #docregion class -export class AppCtorComponent { +export class AppComponent { title: string; myHero: string; diff --git a/aio/content/examples/docs-style-guide/src/app/hero.ts b/aio/content/examples/docs-style-guide/src/app/hero.ts index fadbdbf9ed..e1fc2f4fa9 100644 --- a/aio/content/examples/docs-style-guide/src/app/hero.ts +++ b/aio/content/examples/docs-style-guide/src/app/hero.ts @@ -4,7 +4,7 @@ export class Hero { } export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' } diff --git a/aio/content/examples/dynamic-component-loader/src/app/ad-banner.component.ts b/aio/content/examples/dynamic-component-loader/src/app/ad-banner.component.ts index 0bb04a8590..a802371a30 100644 --- a/aio/content/examples/dynamic-component-loader/src/app/ad-banner.component.ts +++ b/aio/content/examples/dynamic-component-loader/src/app/ad-banner.component.ts @@ -20,7 +20,7 @@ import { AdComponent } from './ad.component'; export class AdBannerComponent implements OnInit, OnDestroy { @Input() ads: AdItem[]; currentAdIndex = -1; - @ViewChild(AdDirective) adHost: AdDirective; + @ViewChild(AdDirective, {static: true}) adHost: AdDirective; interval: any; constructor(private componentFactoryResolver: ComponentFactoryResolver) { } diff --git a/aio/content/examples/elements/example-config.json b/aio/content/examples/elements/example-config.json index cc42921173..e69de29bb2 100644 --- a/aio/content/examples/elements/example-config.json +++ b/aio/content/examples/elements/example-config.json @@ -1,3 +0,0 @@ -{ - "projectType": "elements" -} diff --git a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts index 94ea032ce3..b05b901f6d 100644 --- a/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts +++ b/aio/content/examples/event-binding/src/app/item-detail/item-detail.component.ts @@ -21,7 +21,7 @@ export class ItemDetailComponent { @Output() deleteRequest = new EventEmitter(); delete() { - this.deleteRequest.emit(this.item.name); + this.deleteRequest.emit(this.item); this.displayNone = this.displayNone ? '' : 'none'; this.lineThrough = this.lineThrough ? '' : 'line-through'; } diff --git a/aio/content/examples/feature-modules/e2e/src/app.e2e-spec.ts b/aio/content/examples/feature-modules/e2e/src/app.e2e-spec.ts index 899a1ad037..fbc7dc198a 100644 --- a/aio/content/examples/feature-modules/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/feature-modules/e2e/src/app.e2e-spec.ts @@ -9,7 +9,7 @@ describe('feature-modules App', () => { it('should display message saying app works', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('app works!'); + expect(page.getTitleText()).toEqual('app works!'); }); }); diff --git a/aio/content/examples/getting-started/src/app/cart/cart.component.2.html b/aio/content/examples/getting-started/src/app/cart/cart.component.2.html index 050618d664..829bc0e8fa 100644 --- a/aio/content/examples/getting-started/src/app/cart/cart.component.2.html +++ b/aio/content/examples/getting-started/src/app/cart/cart.component.2.html @@ -9,7 +9,7 @@
- {{ item.name }} + {{ item.name }} {{ item.price | currency }}
- \ No newline at end of file + diff --git a/aio/content/examples/getting-started/src/app/shipping/shipping.component.html b/aio/content/examples/getting-started/src/app/shipping/shipping.component.html index d4566bc318..72fc2d58e1 100644 --- a/aio/content/examples/getting-started/src/app/shipping/shipping.component.html +++ b/aio/content/examples/getting-started/src/app/shipping/shipping.component.html @@ -1,6 +1,6 @@

Shipping Prices

- {{ shipping.type }} + {{ shipping.type }} {{ shipping.price | currency }} -
\ No newline at end of file + diff --git a/aio/content/examples/getting-started/src/app/shipping/shipping.component.ts b/aio/content/examples/getting-started/src/app/shipping/shipping.component.ts index 4a86d5c664..6a04bcb55e 100644 --- a/aio/content/examples/getting-started/src/app/shipping/shipping.component.ts +++ b/aio/content/examples/getting-started/src/app/shipping/shipping.component.ts @@ -16,7 +16,9 @@ export class ShippingComponent { // #enddocregion props // #docregion inject-cart-service - constructor(private cartService: CartService) { + constructor( + private cartService: CartService + ) { // #enddocregion inject-cart-service this.shippingCosts = this.cartService.getShippingPrices(); // #docregion inject-cart-service diff --git a/aio/content/examples/http/e2e/src/app.e2e-spec.ts b/aio/content/examples/http/e2e/src/app.e2e-spec.ts index 9baf2cdc65..091e54acaa 100644 --- a/aio/content/examples/http/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/http/e2e/src/app.e2e-spec.ts @@ -36,7 +36,7 @@ describe('Http Tests', function() { describe('Heroes', () => { it('retrieves the list of heroes at startup', () => { expect(page.heroesListItems.count()).toBe(4); - expect(page.heroesListItems.get(0).getText()).toContain('Mr. Nice'); + expect(page.heroesListItems.get(0).getText()).toContain('Dr Nice'); checkLogForMessage('GET "api/heroes"'); }); diff --git a/aio/content/examples/http/src/app/in-memory-data.service.ts b/aio/content/examples/http/src/app/in-memory-data.service.ts index 606e77f8f5..c74fb2626e 100644 --- a/aio/content/examples/http/src/app/in-memory-data.service.ts +++ b/aio/content/examples/http/src/app/in-memory-data.service.ts @@ -3,7 +3,7 @@ import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { const heroes = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/lazy-loading-ngmodules/e2e/src/app.e2e-spec.ts b/aio/content/examples/lazy-loading-ngmodules/e2e/src/app.e2e-spec.ts index 121d100ccb..c064e540e8 100644 --- a/aio/content/examples/lazy-loading-ngmodules/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/lazy-loading-ngmodules/e2e/src/app.e2e-spec.ts @@ -15,7 +15,7 @@ describe('providers App', () => { it('should display message saying app works', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('Lazy loading feature modules'); + expect(page.getTitleText()).toEqual('Lazy loading feature modules'); }); describe('Customers list', function() { diff --git a/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts b/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts index 073bf73dd6..401aecbb1c 100644 --- a/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts +++ b/aio/content/examples/lazy-loading-ngmodules/src/app/app-routing.module.ts @@ -8,11 +8,11 @@ import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'customers', - loadChildren: './customers/customers.module#CustomersModule' + loadChildren: () => import('./customers/customers.module').then(mod => mod.CustomersModule) }, { path: 'orders', - loadChildren: './orders/orders.module#OrdersModule' + loadChildren: () => import('./orders/orders.module').then(mod => mod.OrdersModule) }, { path: '', diff --git a/aio/content/examples/lifecycle-hooks/src/app/after-content.component.ts b/aio/content/examples/lifecycle-hooks/src/app/after-content.component.ts index 36d2cff20f..858ea12c59 100644 --- a/aio/content/examples/lifecycle-hooks/src/app/after-content.component.ts +++ b/aio/content/examples/lifecycle-hooks/src/app/after-content.component.ts @@ -34,7 +34,7 @@ export class AfterContentComponent implements AfterContentChecked, AfterContentI comment = ''; // Query for a CONTENT child of type `ChildComponent` - @ContentChild(ChildComponent) contentChild: ChildComponent; + @ContentChild(ChildComponent, {static: false}) contentChild: ChildComponent; // #enddocregion hooks constructor(private logger: LoggerService) { diff --git a/aio/content/examples/lifecycle-hooks/src/app/after-view.component.ts b/aio/content/examples/lifecycle-hooks/src/app/after-view.component.ts index fd09962406..59c472b37a 100644 --- a/aio/content/examples/lifecycle-hooks/src/app/after-view.component.ts +++ b/aio/content/examples/lifecycle-hooks/src/app/after-view.component.ts @@ -35,7 +35,7 @@ export class AfterViewComponent implements AfterViewChecked, AfterViewInit { private prevHero = ''; // Query for a VIEW child of type `ChildViewComponent` - @ViewChild(ChildViewComponent) viewChild: ChildViewComponent; + @ViewChild(ChildViewComponent, {static: false}) viewChild: ChildViewComponent; // #enddocregion hooks constructor(private logger: LoggerService) { diff --git a/aio/content/examples/lifecycle-hooks/src/app/do-check.component.ts b/aio/content/examples/lifecycle-hooks/src/app/do-check.component.ts index 4b8a088b9a..098ee941d2 100644 --- a/aio/content/examples/lifecycle-hooks/src/app/do-check.component.ts +++ b/aio/content/examples/lifecycle-hooks/src/app/do-check.component.ts @@ -81,7 +81,7 @@ export class DoCheckParentComponent { hero: Hero; power: string; title = 'DoCheck'; - @ViewChild(DoCheckComponent) childView: DoCheckComponent; + @ViewChild(DoCheckComponent, {static: false}) childView: DoCheckComponent; constructor() { this.reset(); } diff --git a/aio/content/examples/lifecycle-hooks/src/app/on-changes.component.ts b/aio/content/examples/lifecycle-hooks/src/app/on-changes.component.ts index 7b9cda05ef..09f3169cc2 100644 --- a/aio/content/examples/lifecycle-hooks/src/app/on-changes.component.ts +++ b/aio/content/examples/lifecycle-hooks/src/app/on-changes.component.ts @@ -55,7 +55,7 @@ export class OnChangesParentComponent { hero: Hero; power: string; title = 'OnChanges'; - @ViewChild(OnChangesComponent) childView: OnChangesComponent; + @ViewChild(OnChangesComponent, {static: false}) childView: OnChangesComponent; constructor() { this.reset(); diff --git a/aio/content/examples/ngcontainer/src/app/app.component.ts b/aio/content/examples/ngcontainer/src/app/app.component.ts index 068d1896d8..e17f5ecdf9 100644 --- a/aio/content/examples/ngcontainer/src/app/app.component.ts +++ b/aio/content/examples/ngcontainer/src/app/app.component.ts @@ -4,7 +4,7 @@ import { Component } from '@angular/core'; import { heroes } from './hero'; @Component({ - selector: 'my-app', + selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) diff --git a/aio/content/examples/ngcontainer/src/app/hero.ts b/aio/content/examples/ngcontainer/src/app/hero.ts index 28b435252d..8619853380 100644 --- a/aio/content/examples/ngcontainer/src/app/hero.ts +++ b/aio/content/examples/ngcontainer/src/app/hero.ts @@ -6,7 +6,7 @@ export class Hero { } export const heroes: Hero[] = [ - { id: 1, name: 'Mr. Nice', emotion: 'happy' }, + { id: 1, name: 'Dr Nice', emotion: 'happy' }, { id: 2, name: 'Narco', emotion: 'sad' }, { id: 3, name: 'Windstorm', emotion: 'confused' }, { id: 4, name: 'Magneta' } diff --git a/aio/content/examples/ngcontainer/src/index.html b/aio/content/examples/ngcontainer/src/index.html index 38a7c1dff1..d189d0a056 100644 --- a/aio/content/examples/ngcontainer/src/index.html +++ b/aio/content/examples/ngcontainer/src/index.html @@ -6,7 +6,7 @@ - Loading... + Loading... diff --git a/aio/content/examples/ngmodules/src/app/app-routing.module.ts b/aio/content/examples/ngmodules/src/app/app-routing.module.ts index 2fea32093a..3ae0d3d346 100644 --- a/aio/content/examples/ngmodules/src/app/app-routing.module.ts +++ b/aio/content/examples/ngmodules/src/app/app-routing.module.ts @@ -3,8 +3,8 @@ import { Routes, RouterModule } from '@angular/router'; export const routes: Routes = [ { path: '', redirectTo: 'contact', pathMatch: 'full'}, - { path: 'items', loadChildren: './items/items.module#ItemsModule' }, - { path: 'customers', loadChildren: './customers/customers.module#CustomersModule' } + { path: 'items', loadChildren: () => import('./items/items.module').then(mod => mod.ItemsModule) }, + { path: 'customers', loadChildren: () => import('./customers/customers.module').then(mod => mod.CustomersModule) } ]; @NgModule({ diff --git a/aio/content/examples/pipes/src/app/exponential-strength.pipe.ts b/aio/content/examples/pipes/src/app/exponential-strength.pipe.ts index d277483853..de8a8fca2d 100644 --- a/aio/content/examples/pipes/src/app/exponential-strength.pipe.ts +++ b/aio/content/examples/pipes/src/app/exponential-strength.pipe.ts @@ -11,8 +11,7 @@ import { Pipe, PipeTransform } from '@angular/core'; */ @Pipe({name: 'exponentialStrength'}) export class ExponentialStrengthPipe implements PipeTransform { - transform(value: number, exponent: string): number { - let exp = parseFloat(exponent); - return Math.pow(value, isNaN(exp) ? 1 : exp); + transform(value: number, exponent?: number): number { + return Math.pow(value, isNaN(exponent) ? 1 : exponent); } } diff --git a/aio/content/examples/providers/e2e/src/app.e2e-spec.ts b/aio/content/examples/providers/e2e/src/app.e2e-spec.ts index 46108fd9dc..758c5fa0e5 100644 --- a/aio/content/examples/providers/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/providers/e2e/src/app.e2e-spec.ts @@ -23,7 +23,7 @@ describe('providers App', () => { it('should display header that says Users list', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('Users list'); + expect(page.getTitleText()).toEqual('Users list'); }); diff --git a/aio/content/examples/router/src/app/app-routing.module.5.ts b/aio/content/examples/router/src/app/app-routing.module.5.ts index a120771875..e99a6cb3c4 100644 --- a/aio/content/examples/router/src/app/app-routing.module.5.ts +++ b/aio/content/examples/router/src/app/app-routing.module.5.ts @@ -20,7 +20,7 @@ const appRoutes: Routes = [ // #docregion admin, admin-1 { path: 'admin', - loadChildren: './admin/admin.module#AdminModule', + loadChildren: () => import('./admin/admin.module').then(mod => mod.AdminModule), // #enddocregion admin-1 canLoad: [AuthGuard] // #docregion admin-1 diff --git a/aio/content/examples/router/src/app/app-routing.module.6.ts b/aio/content/examples/router/src/app/app-routing.module.6.ts index e1c81f2498..d060fad168 100644 --- a/aio/content/examples/router/src/app/app-routing.module.6.ts +++ b/aio/content/examples/router/src/app/app-routing.module.6.ts @@ -21,12 +21,12 @@ const appRoutes: Routes = [ }, { path: 'admin', - loadChildren: './admin/admin.module#AdminModule', + loadChildren: () => import('./admin/admin.module').then(mod => mod.AdminModule), canLoad: [AuthGuard] }, { path: 'crisis-center', - loadChildren: './crisis-center/crisis-center.module#CrisisCenterModule' + loadChildren: () => import('./crisis-center/crisis-center.module').then(mod => mod.CrisisCenterModule) }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } diff --git a/aio/content/examples/router/src/app/app-routing.module.ts b/aio/content/examples/router/src/app/app-routing.module.ts index ffad588287..fc8bcb3b44 100644 --- a/aio/content/examples/router/src/app/app-routing.module.ts +++ b/aio/content/examples/router/src/app/app-routing.module.ts @@ -17,13 +17,13 @@ const appRoutes: Routes = [ }, { path: 'admin', - loadChildren: './admin/admin.module#AdminModule', + loadChildren: () => import('./admin/admin.module').then(mod => mod.AdminModule), canLoad: [AuthGuard] }, // #docregion preload-v2 { path: 'crisis-center', - loadChildren: './crisis-center/crisis-center.module#CrisisCenterModule', + loadChildren: () => import('./crisis-center/crisis-center.module').then(mod => mod.CrisisCenterModule), data: { preload: true } }, // #enddocregion preload-v2 diff --git a/aio/content/examples/router/src/app/heroes/mock-heroes.ts b/aio/content/examples/router/src/app/heroes/mock-heroes.ts index e84c2fd2b0..27ed1ffbff 100644 --- a/aio/content/examples/router/src/app/heroes/mock-heroes.ts +++ b/aio/content/examples/router/src/app/heroes/mock-heroes.ts @@ -1,7 +1,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/service-worker-getting-started/e2e/src/app.e2e-spec.ts b/aio/content/examples/service-worker-getting-started/e2e/src/app.e2e-spec.ts index c221f0ebb6..033a0af6aa 100755 --- a/aio/content/examples/service-worker-getting-started/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/service-worker-getting-started/e2e/src/app.e2e-spec.ts @@ -11,7 +11,7 @@ describe('sw-example App', () => { it('should display welcome message', () => { page.navigateTo(); - expect(page.getParagraphText()).toEqual('Welcome to Service Workers!'); + expect(page.getTitleText()).toEqual('Welcome to Service Workers!'); }); it('should display the Angular logo', () => { diff --git a/aio/content/examples/setup/src/app/app.component.ts b/aio/content/examples/setup/src/app/app.component.ts index 1ef28fc5c4..10bb6053e2 100644 --- a/aio/content/examples/setup/src/app/app.component.ts +++ b/aio/content/examples/setup/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'my-app', + selector: 'app-root', template: `

Hello {{name}}

` }) export class AppComponent { name = 'Angular'; } diff --git a/aio/content/examples/setup/src/index.html b/aio/content/examples/setup/src/index.html index 58e5112308..d28fe05691 100644 --- a/aio/content/examples/setup/src/index.html +++ b/aio/content/examples/setup/src/index.html @@ -20,9 +20,9 @@ - - - + + + diff --git a/aio/content/examples/structural-directives/e2e/src/app.e2e-spec.ts b/aio/content/examples/structural-directives/e2e/src/app.e2e-spec.ts index a877fb3223..9023c4ea89 100644 --- a/aio/content/examples/structural-directives/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/structural-directives/e2e/src/app.e2e-spec.ts @@ -10,12 +10,12 @@ describe('Structural Directives', function () { it('first div should show hero name with *ngIf', function () { const allDivs = element.all(by.tagName('div')); - expect(allDivs.get(0).getText()).toEqual('Mr. Nice'); + expect(allDivs.get(0).getText()).toEqual('Dr Nice'); }); it('first li should show hero name with *ngFor', function () { const allLis = element.all(by.tagName('li')); - expect(allLis.get(0).getText()).toEqual('Mr. Nice'); + expect(allLis.get(0).getText()).toEqual('Dr Nice'); }); it('ngSwitch have two instances', function () { diff --git a/aio/content/examples/structural-directives/src/app/hero.ts b/aio/content/examples/structural-directives/src/app/hero.ts index 89f0cbfdf8..4056351495 100644 --- a/aio/content/examples/structural-directives/src/app/hero.ts +++ b/aio/content/examples/structural-directives/src/app/hero.ts @@ -6,7 +6,7 @@ export class Hero { } export const heroes: Hero[] = [ - { id: 1, name: 'Mr. Nice', emotion: 'happy'}, + { id: 1, name: 'Dr Nice', emotion: 'happy'}, { id: 2, name: 'Narco', emotion: 'sad' }, { id: 3, name: 'Windstorm', emotion: 'confused' }, { id: 4, name: 'Magneta'} diff --git a/aio/content/examples/styleguide/src/01-01/app/heroes/hero.component.avoid.ts b/aio/content/examples/styleguide/src/01-01/app/heroes/hero.component.avoid.ts index 2ec03ca8b3..7e0775b858 100644 --- a/aio/content/examples/styleguide/src/01-01/app/heroes/hero.component.avoid.ts +++ b/aio/content/examples/styleguide/src/01-01/app/heroes/hero.component.avoid.ts @@ -10,7 +10,7 @@ class Hero { } @Component({ - selector: 'my-app', + selector: 'app-root', template: `

{{title}}

{{heroes | json}}
diff --git a/aio/content/examples/template-syntax/src/app/hero-form.component.ts b/aio/content/examples/template-syntax/src/app/hero-form.component.ts index 63040131ff..173cc137de 100644 --- a/aio/content/examples/template-syntax/src/app/hero-form.component.ts +++ b/aio/content/examples/template-syntax/src/app/hero-form.component.ts @@ -13,7 +13,7 @@ import { Hero } from './hero'; }) export class HeroFormComponent { @Input() hero: Hero; - @ViewChild('heroForm') form: NgForm; + @ViewChild('heroForm', {static: false}) form: NgForm; private _submitMessage = ''; diff --git a/aio/content/examples/template-syntax/src/app/hero.ts b/aio/content/examples/template-syntax/src/app/hero.ts index f8cc3b16a6..612c1035cb 100644 --- a/aio/content/examples/template-syntax/src/app/hero.ts +++ b/aio/content/examples/template-syntax/src/app/hero.ts @@ -10,7 +10,7 @@ export class Hero { 'http://www.imdb.com/title/tt0065832/', 325 ), - new Hero(1, 'Mr. Nice', 'happy'), + new Hero(1, 'Dr Nice', 'happy'), new Hero(2, 'Narco', 'sad' ), new Hero(3, 'Windstorm', 'confused' ), new Hero(4, 'Magneta') diff --git a/aio/content/examples/testing/src/app/app-routing.module.ts b/aio/content/examples/testing/src/app/app-routing.module.ts index 7630c216f6..98c92a1f5a 100644 --- a/aio/content/examples/testing/src/app/app-routing.module.ts +++ b/aio/content/examples/testing/src/app/app-routing.module.ts @@ -8,7 +8,7 @@ import { AboutComponent } from './about/about.component'; RouterModule.forRoot([ { path: '', redirectTo: 'dashboard', pathMatch: 'full'}, { path: 'about', component: AboutComponent }, - { path: 'heroes', loadChildren: './hero/hero.module#HeroModule'} + { path: 'heroes', loadChildren: () => import('./hero/hero.module').then(mod => mod.HeroModule)} ]) ], exports: [ RouterModule ] // re-export the module declarations diff --git a/aio/content/examples/testing/src/app/in-memory-data.service.ts b/aio/content/examples/testing/src/app/in-memory-data.service.ts index 81f26b674a..d3827fcd2b 100644 --- a/aio/content/examples/testing/src/app/in-memory-data.service.ts +++ b/aio/content/examples/testing/src/app/in-memory-data.service.ts @@ -9,7 +9,7 @@ const maxQuotes = Infinity; // 0; export class InMemoryDataService implements InMemoryDbService { createDb() { const heroes = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/testing/src/app/model/testing/index.ts b/aio/content/examples/testing/src/app/model/testing/index.ts index 6da76e67db..898f64c278 100644 --- a/aio/content/examples/testing/src/app/model/testing/index.ts +++ b/aio/content/examples/testing/src/app/model/testing/index.ts @@ -1 +1 @@ -export * from './fake-hero.service'; +export * from './test-hero.service'; diff --git a/aio/content/examples/testing/src/app/shared/canvas.component.ts b/aio/content/examples/testing/src/app/shared/canvas.component.ts index 0f32dbeeb6..584607963e 100644 --- a/aio/content/examples/testing/src/app/shared/canvas.component.ts +++ b/aio/content/examples/testing/src/app/shared/canvas.component.ts @@ -6,7 +6,7 @@ import { Component, AfterViewInit, ViewChild } from '@angular/core'; }) export class CanvasComponent implements AfterViewInit { blobSize: number; - @ViewChild('sampleCanvas') sampleCanvas; + @ViewChild('sampleCanvas', {static: false}) sampleCanvas; constructor() { } diff --git a/aio/content/examples/toh-pt0/src/styles.1.css b/aio/content/examples/toh-pt0/src/styles.1.css index 264b345eba..1821a5fc8d 100644 --- a/aio/content/examples/toh-pt0/src/styles.1.css +++ b/aio/content/examples/toh-pt0/src/styles.1.css @@ -13,7 +13,7 @@ body { margin: 2em; } body, input[type="text"], button { - color: #888; + color: #333; font-family: Cambria, Georgia; } /* everywhere else */ diff --git a/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.css b/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.css index e956eab987..9121bec918 100644 --- a/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.css +++ b/aio/content/examples/toh-pt2/src/app/heroes/heroes.component.css @@ -37,7 +37,7 @@ font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; + background-color:#405061; line-height: 1em; position: relative; left: -1px; diff --git a/aio/content/examples/toh-pt2/src/app/mock-heroes.ts b/aio/content/examples/toh-pt2/src/app/mock-heroes.ts index e84c2fd2b0..27ed1ffbff 100644 --- a/aio/content/examples/toh-pt2/src/app/mock-heroes.ts +++ b/aio/content/examples/toh-pt2/src/app/mock-heroes.ts @@ -1,7 +1,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/toh-pt3/src/app/heroes/heroes.component.css b/aio/content/examples/toh-pt3/src/app/heroes/heroes.component.css index e956eab987..9121bec918 100644 --- a/aio/content/examples/toh-pt3/src/app/heroes/heroes.component.css +++ b/aio/content/examples/toh-pt3/src/app/heroes/heroes.component.css @@ -37,7 +37,7 @@ font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; + background-color:#405061; line-height: 1em; position: relative; left: -1px; diff --git a/aio/content/examples/toh-pt3/src/app/mock-heroes.ts b/aio/content/examples/toh-pt3/src/app/mock-heroes.ts index e84c2fd2b0..27ed1ffbff 100644 --- a/aio/content/examples/toh-pt3/src/app/mock-heroes.ts +++ b/aio/content/examples/toh-pt3/src/app/mock-heroes.ts @@ -1,7 +1,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/toh-pt4/src/app/heroes/heroes.component.css b/aio/content/examples/toh-pt4/src/app/heroes/heroes.component.css index 4bef1bb3c4..1d73d53659 100644 --- a/aio/content/examples/toh-pt4/src/app/heroes/heroes.component.css +++ b/aio/content/examples/toh-pt4/src/app/heroes/heroes.component.css @@ -37,7 +37,7 @@ font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; + background-color:#405061; line-height: 1em; position: relative; left: -1px; diff --git a/aio/content/examples/toh-pt4/src/app/messages/messages.component.css b/aio/content/examples/toh-pt4/src/app/messages/messages.component.css index d08d9be581..de06809856 100644 --- a/aio/content/examples/toh-pt4/src/app/messages/messages.component.css +++ b/aio/content/examples/toh-pt4/src/app/messages/messages.component.css @@ -30,6 +30,6 @@ button:disabled { cursor: auto; } button.clear { - color: #888; + color: #333; margin-bottom: 12px; } diff --git a/aio/content/examples/toh-pt4/src/app/mock-heroes.ts b/aio/content/examples/toh-pt4/src/app/mock-heroes.ts index 1771a7103b..cd3506f574 100644 --- a/aio/content/examples/toh-pt4/src/app/mock-heroes.ts +++ b/aio/content/examples/toh-pt4/src/app/mock-heroes.ts @@ -2,7 +2,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/toh-pt5/src/app/app.component.css b/aio/content/examples/toh-pt5/src/app/app.component.css index 7cf19e6ede..c62f266159 100644 --- a/aio/content/examples/toh-pt5/src/app/app.component.css +++ b/aio/content/examples/toh-pt5/src/app/app.component.css @@ -1,7 +1,6 @@ /* AppComponent's private CSS styles */ h1 { font-size: 1.2em; - color: #999; margin-bottom: 0; } h2 { @@ -18,7 +17,7 @@ nav a { border-radius: 4px; } nav a:visited, a:link { - color: #607d8b; + color: #334953; } nav a:hover { color: #039be5; diff --git a/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.css b/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.css index 68e65dda00..4b7ec1c3ee 100644 --- a/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.css +++ b/aio/content/examples/toh-pt5/src/app/dashboard/dashboard.component.css @@ -34,7 +34,7 @@ h4 { color: #eee; max-height: 120px; min-width: 120px; - background-color: #607d8b; + background-color: #3f525c; border-radius: 2px; } .module:hover { diff --git a/aio/content/examples/toh-pt5/src/app/heroes/heroes.component.css b/aio/content/examples/toh-pt5/src/app/heroes/heroes.component.css index 91e0badceb..2ad3fefb31 100644 --- a/aio/content/examples/toh-pt5/src/app/heroes/heroes.component.css +++ b/aio/content/examples/toh-pt5/src/app/heroes/heroes.component.css @@ -22,7 +22,7 @@ } .heroes a { - color: #888; + color: #333; text-decoration: none; position: relative; display: block; @@ -38,7 +38,7 @@ font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; + background-color:#405061; line-height: 1em; position: relative; left: -1px; diff --git a/aio/content/examples/toh-pt5/src/app/messages/messages.component.css b/aio/content/examples/toh-pt5/src/app/messages/messages.component.css index d08d9be581..de06809856 100644 --- a/aio/content/examples/toh-pt5/src/app/messages/messages.component.css +++ b/aio/content/examples/toh-pt5/src/app/messages/messages.component.css @@ -30,6 +30,6 @@ button:disabled { cursor: auto; } button.clear { - color: #888; + color: #333; margin-bottom: 12px; } diff --git a/aio/content/examples/toh-pt5/src/app/mock-heroes.ts b/aio/content/examples/toh-pt5/src/app/mock-heroes.ts index 1771a7103b..cd3506f574 100644 --- a/aio/content/examples/toh-pt5/src/app/mock-heroes.ts +++ b/aio/content/examples/toh-pt5/src/app/mock-heroes.ts @@ -2,7 +2,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/toh-pt6/src/app/app.component.css b/aio/content/examples/toh-pt6/src/app/app.component.css index bf741e4575..c1d8721226 100644 --- a/aio/content/examples/toh-pt6/src/app/app.component.css +++ b/aio/content/examples/toh-pt6/src/app/app.component.css @@ -1,7 +1,6 @@ /* AppComponent's private CSS styles */ h1 { font-size: 1.2em; - color: #999; margin-bottom: 0; } h2 { @@ -18,7 +17,7 @@ nav a { border-radius: 4px; } nav a:visited, a:link { - color: #607D8B; + color: #334953; } nav a:hover { color: #039be5; diff --git a/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.css b/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.css index 6222c23bb7..938b84b2b0 100644 --- a/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.css +++ b/aio/content/examples/toh-pt6/src/app/dashboard/dashboard.component.css @@ -34,7 +34,7 @@ h4 { color: #eee; max-height: 120px; min-width: 120px; - background-color: #607D8B; + background-color: #3f525c; border-radius: 2px; } .module:hover { diff --git a/aio/content/examples/toh-pt6/src/app/hero-search/hero-search.component.html b/aio/content/examples/toh-pt6/src/app/hero-search/hero-search.component.html index 52f3e958e3..de72fd791f 100644 --- a/aio/content/examples/toh-pt6/src/app/hero-search/hero-search.component.html +++ b/aio/content/examples/toh-pt6/src/app/hero-search/hero-search.component.html @@ -1,5 +1,5 @@
-

Hero Search

+

diff --git a/aio/content/examples/toh-pt6/src/app/heroes/heroes.component.css b/aio/content/examples/toh-pt6/src/app/heroes/heroes.component.css index c345d1d970..0b13f0fe4c 100644 --- a/aio/content/examples/toh-pt6/src/app/heroes/heroes.component.css +++ b/aio/content/examples/toh-pt6/src/app/heroes/heroes.component.css @@ -22,7 +22,7 @@ } .heroes a { - color: #888; + color: #333; text-decoration: none; position: relative; display: block; @@ -38,7 +38,7 @@ font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; - background-color: #607D8B; + background-color:#405061; line-height: 1em; position: relative; left: -1px; diff --git a/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts b/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts index c590be5d65..cb95d5eae5 100644 --- a/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts +++ b/aio/content/examples/toh-pt6/src/app/in-memory-data.service.ts @@ -9,7 +9,7 @@ import { Injectable } from '@angular/core'; export class InMemoryDataService implements InMemoryDbService { createDb() { const heroes = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/toh-pt6/src/app/messages/messages.component.css b/aio/content/examples/toh-pt6/src/app/messages/messages.component.css index d08d9be581..de06809856 100644 --- a/aio/content/examples/toh-pt6/src/app/messages/messages.component.css +++ b/aio/content/examples/toh-pt6/src/app/messages/messages.component.css @@ -30,6 +30,6 @@ button:disabled { cursor: auto; } button.clear { - color: #888; + color: #333; margin-bottom: 12px; } diff --git a/aio/content/examples/toh-pt6/src/app/mock-heroes.ts b/aio/content/examples/toh-pt6/src/app/mock-heroes.ts index 1771a7103b..cd3506f574 100644 --- a/aio/content/examples/toh-pt6/src/app/mock-heroes.ts +++ b/aio/content/examples/toh-pt6/src/app/mock-heroes.ts @@ -2,7 +2,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/universal/src/app/in-memory-data.service.ts b/aio/content/examples/universal/src/app/in-memory-data.service.ts index ff1f190f83..b23507887e 100644 --- a/aio/content/examples/universal/src/app/in-memory-data.service.ts +++ b/aio/content/examples/universal/src/app/in-memory-data.service.ts @@ -3,7 +3,7 @@ import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { const heroes = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/universal/src/app/mock-heroes.ts b/aio/content/examples/universal/src/app/mock-heroes.ts index e84c2fd2b0..27ed1ffbff 100644 --- a/aio/content/examples/universal/src/app/mock-heroes.ts +++ b/aio/content/examples/universal/src/app/mock-heroes.ts @@ -1,7 +1,7 @@ import { Hero } from './hero'; export const HEROES: Hero[] = [ - { id: 11, name: 'Mr. Nice' }, + { id: 11, name: 'Dr Nice' }, { id: 12, name: 'Narco' }, { id: 13, name: 'Bombasto' }, { id: 14, name: 'Celeritas' }, diff --git a/aio/content/examples/universal/src/tsconfig.server.json b/aio/content/examples/universal/tsconfig.server.json similarity index 69% rename from aio/content/examples/universal/src/tsconfig.server.json rename to aio/content/examples/universal/tsconfig.server.json index 4401f4ca65..5a3e958cb0 100644 --- a/aio/content/examples/universal/src/tsconfig.server.json +++ b/aio/content/examples/universal/tsconfig.server.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "./", @@ -11,6 +11,6 @@ "**/*.spec.ts" ], "angularCompilerOptions": { - "entryModule": "app/app.server.module#AppServerModule" + "entryModule": "src/app/app.server.module#AppServerModule" } } diff --git a/aio/content/examples/upgrade-lazy-load-ajs/example-config.json b/aio/content/examples/upgrade-lazy-load-ajs/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/angular-js/angular-js.component.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/angular-js/angular-js.component.ts new file mode 100644 index 0000000000..bc5d22c0f0 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/angular-js/angular-js.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit, ElementRef } from '@angular/core'; +import { LazyLoaderService } from '../lazy-loader.service'; + +@Component({ + selector: 'app-angular-js', + template: '
' +}) +export class AngularJSComponent implements OnInit { + constructor(private lazyLoader: LazyLoaderService, private elRef: ElementRef) {} + + ngOnInit() { + this.lazyLoader.load(this.elRef.nativeElement); + } +} diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/angularjs-app/index.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/angularjs-app/index.ts new file mode 100644 index 0000000000..4d43a7fa09 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/angularjs-app/index.ts @@ -0,0 +1,29 @@ +import * as angular from 'angular'; +import 'angular-route'; + +const appName = 'myApp'; + +angular.module(appName, [ + 'ngRoute' +]) +.config(['$routeProvider', '$locationProvider', + function config($routeProvider, $locationProvider) { + $locationProvider.html5Mode(true); + + $routeProvider. + when('/users', { + template: ` +

+ Users Page +

+ ` + }). + otherwise({ + template: '' + }); + }] +); + +export function bootstrap(el: HTMLElement) { + return angular.bootstrap(el, [appName]); +} diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app-routing.module.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app-routing.module.ts new file mode 100644 index 0000000000..5cc7055597 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app-routing.module.ts @@ -0,0 +1,31 @@ +// #docplaster +// #docregion +import { NgModule } from '@angular/core'; +import { Routes, RouterModule, UrlSegment } from '@angular/router'; +import { AngularJSComponent } from './angular-js/angular-js.component'; +import { HomeComponent } from './home/home.component'; +import { App404Component } from './app404/app404.component'; + +// Match any URL that starts with `users` +// #docregion matcher +export function isAngularJSUrl(url: UrlSegment[]) { + return url.length > 0 && url[0].path.startsWith('users') ? ({consumed: url}) : null; +} +// #enddocregion matcher + +export const routes: Routes = [ + // Routes rendered by Angular + { path: '', component: HomeComponent }, + + // AngularJS routes + { matcher: isAngularJSUrl, component: AngularJSComponent }, + + // Catch-all route + { path: '**', component: App404Component } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.css b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.html b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.html new file mode 100644 index 0000000000..51c9edae12 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.html @@ -0,0 +1,9 @@ +Lazy Loading AngularJS + + + + diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.ts new file mode 100644 index 0000000000..400d937a2f --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { } diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.module.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.module.ts new file mode 100644 index 0000000000..4e199e90bf --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app.module.ts @@ -0,0 +1,25 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { AngularJSComponent } from './angular-js/angular-js.component'; +import { HomeComponent } from './home/home.component'; +import { App404Component } from './app404/app404.component'; + + +@NgModule({ + declarations: [ + AppComponent, + AngularJSComponent, + HomeComponent, + App404Component + ], + imports: [ + BrowserModule, + AppRoutingModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.css b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.html b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.html new file mode 100644 index 0000000000..27e1f55db9 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.html @@ -0,0 +1,3 @@ +

+ Angular 404 +

diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.ts new file mode 100644 index 0000000000..9adfed8166 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/app404/app404.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-app404', + templateUrl: './app404.component.html', + styleUrls: ['./app404.component.css'] +}) +export class App404Component implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.css b/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.html b/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.html new file mode 100644 index 0000000000..a2bf06c9de --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.html @@ -0,0 +1,3 @@ +

+ Angular Home +

diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.ts new file mode 100644 index 0000000000..33fd77077e --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/home/home.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] +}) +export class HomeComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/aio/content/examples/upgrade-lazy-load-ajs/src/app/lazy-loader.service.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/app/lazy-loader.service.ts new file mode 100644 index 0000000000..50c8492fb9 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/app/lazy-loader.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LazyLoaderService { + bootstrapped = false; + + load(el: HTMLElement): void { + if (this.bootstrapped) { + return; + } + + import('./angularjs-app').then(app => { + try { + app.bootstrap(el); + this.bootstrapped = true; + } catch (e) { + console.error(e); + } + }); + } +} diff --git a/aio/content/examples/cli-quickstart/src/index.html b/aio/content/examples/upgrade-lazy-load-ajs/src/index.html similarity index 88% rename from aio/content/examples/cli-quickstart/src/index.html rename to aio/content/examples/upgrade-lazy-load-ajs/src/index.html index 0450fa57c3..53a90fd4e6 100644 --- a/aio/content/examples/cli-quickstart/src/index.html +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/index.html @@ -2,7 +2,7 @@ - MyApp + AngularJS Hybrid diff --git a/aio/content/examples/cli-quickstart/src/main.ts b/aio/content/examples/upgrade-lazy-load-ajs/src/main.ts similarity index 76% rename from aio/content/examples/cli-quickstart/src/main.ts rename to aio/content/examples/upgrade-lazy-load-ajs/src/main.ts index a9ca1caf8c..c7b673cf44 100644 --- a/aio/content/examples/cli-quickstart/src/main.ts +++ b/aio/content/examples/upgrade-lazy-load-ajs/src/main.ts @@ -8,4 +8,5 @@ if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule); +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/aio/content/examples/upgrade-lazy-load-ajs/zipper.json b/aio/content/examples/upgrade-lazy-load-ajs/zipper.json new file mode 100644 index 0000000000..f86b7903a7 --- /dev/null +++ b/aio/content/examples/upgrade-lazy-load-ajs/zipper.json @@ -0,0 +1,7 @@ +{ + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ] +} diff --git a/aio/content/guide/animations.md b/aio/content/guide/animations.md index eaee6a92c7..888998c557 100644 --- a/aio/content/guide/animations.md +++ b/aio/content/guide/animations.md @@ -229,7 +229,7 @@ The third argument, `easing`, controls how the animation [accelerates and decele 运行 200 毫秒,不等待。按照标准曲线运动,开始很慢,中间加速,最后逐渐减速:`'0.2s ease-in-out'` -* Start immediately, run for 200ms. Use a acceleration curve to start slow and end at full velocity: `'0.2s ease-in'` +* Start immediately, run for 200ms. Use an acceleration curve to start slow and end at full velocity: `'0.2s ease-in'` 立即开始,运行 200 毫秒。按照加速曲线运动,开始很慢,最后达到全速:`'0.2s ease-in'` diff --git a/aio/content/guide/architecture-components.md b/aio/content/guide/architecture-components.md index a581e1c5da..26f7c29b5c 100644 --- a/aio/content/guide/architecture-components.md +++ b/aio/content/guide/architecture-components.md @@ -151,7 +151,7 @@ Notice how custom components like this mix seamlessly with native HTML in the sa ### 数据绑定 -Without a framework, you would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push and pull logic by hand is tedious, error-prone, and a nightmare to read, as any experienced jQuery programmer can attest. +Without a framework, you would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push and pull logic by hand is tedious, error-prone, and a nightmare to read, as any experienced front-end JavaScript programmer can attest. 如果没有框架,你就要自己负责把数据值推送到 HTML 控件中,并把来自用户的响应转换成动作和对值的更新。 手动写这种数据推拉逻辑会很枯燥、容易出错,难以阅读 —— 用过 jQuery 的程序员一定深有体会。 diff --git a/aio/content/guide/attribute-directives.md b/aio/content/guide/attribute-directives.md index 53c071c491..efeeaa5773 100644 --- a/aio/content/guide/attribute-directives.md +++ b/aio/content/guide/attribute-directives.md @@ -33,7 +33,7 @@ There are three kinds of directives in Angular: 属性型指令 — 改变元素、组件或其它指令的外观和行为的指令。 *Components* are the most common of the three directives. -You saw a component for the first time in the [Getting Started](guide/quickstart). +You saw a component for the first time in the [Getting Started](start "Getting Started with Angular") tutorial. *组件*是这三种指令中最常用的。 你在[快速上手](guide/quickstart)例子中第一次见到组件。 diff --git a/aio/content/guide/cheatsheet.md b/aio/content/guide/cheatsheet.md index 44c88ea671..cf6226b7de 100644 --- a/aio/content/guide/cheatsheet.md +++ b/aio/content/guide/cheatsheet.md @@ -1263,24 +1263,20 @@ so the @Directive configuration applies to components as well

- class CanActivateGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}

{ path: ..., canActivate: [CanActivateGuard] }
+ class CanActivateGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }
}

{ path: ..., canActivate: [CanActivateGuard] }
-

An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean or an Observable/Promise that resolves to a boolean.

+

An interface for defining a class that the router should call first to determine if it should activate this component. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.

用来定义类的接口。路由器会首先调用本接口来决定是否激活该路由。应该返回一个 boolean 或能解析成 booleanObservable/Promise

- - - - - class CanDeactivateGuard implements CanDeactivate<T> {
canDeactivate(
component: T,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}

{ path: ..., canDeactivate: [CanDeactivateGuard] }
- +class CanDeactivateGuard implements CanDeactivate<T> {
canDeactivate(
component: T,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }
}

{ path: ..., canDeactivate: [CanDeactivateGuard] }
+

An interface for defining a class that the router should call first to determine if it should deactivate this component after a navigation. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.

@@ -1292,11 +1288,8 @@ so the @Directive configuration applies to components as well

- - - - class CanActivateChildGuard implements CanActivateChild {
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean { ... }
}

{ path: ..., canActivateChild: [CanActivateGuard],
children: ... }
- +class CanActivateChildGuard implements CanActivateChild {
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }
}

{ path: ..., canActivateChild: [CanActivateGuard],
children: ... }
+

An interface for defining a class that the router should call first to determine if it should activate the child route. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.

@@ -1327,16 +1320,15 @@ so the @Directive configuration applies to components as well

- class CanLoadGuard implements CanLoad {
canLoad(
route: Route
): Observable<boolean>|Promise<boolean>|boolean { ... }
}

{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }
+ class CanLoadGuard implements CanLoad {
canLoad(
route: Route
): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree { ... }
}

{ path: ..., canLoad: [CanLoadGuard], loadChildren: ... }
-

An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean or an Observable/Promise that resolves to a boolean.

+

An interface for defining a class that the router should call first to check if the lazy loaded module should be loaded. Should return a boolean|UrlTree or an Observable/Promise that resolves to a boolean|UrlTree.

用来定义类的接口。路由器会首先调用它来决定是否应该加载一个惰性加载模块。应该返回一个 boolean 或能解析成 booleanObservable/Promise

- diff --git a/aio/content/guide/cli-builder.md b/aio/content/guide/cli-builder.md new file mode 100644 index 0000000000..d78cfa4720 --- /dev/null +++ b/aio/content/guide/cli-builder.md @@ -0,0 +1,595 @@ +# Angular CLI Builders + +A number of Angular CLI commands run a complex process on your code, such as linting, building, or testing. +The commands use an internal tool called Architect to run *CLI builders*, which apply another tool to accomplish the desired task. + +With Angular version 8, the CLI Builder API is stable and available to developers who want to customize the Angular CLI by adding or modifying commands. For example, you could supply a builder to perform an entirely new task, or to change which third-party tool is used by an existing command. + +This document explains how CLI builders integrate with the workspace configuration file, and shows how you can create your own builder. + +
+ + You can find the code from the examples used here in [this GitHub repository](https://github.com/mgechev/cli-builders-demo). + +
+ +## CLI builders + +The internal Architect tool delegates work to handler functions called [*builders*](guide/glossary#builder). +A builder handler function receives two arguments; a set of input `options` (a JSON object), and a `context` (a `BuilderContext` object). + +The separation of concerns here is the same as with [schematics](guide/glossary#schematic), which are used for other CLI commands that touch your code (such as `ng generate`). + +* Options are given by the CLI user, context is provided by and provides access to the CLI Builder API, and the developer provides the behavior. + +* The `BuilderContext` object provides access to the scheduling method, `BuilderContext.scheduleTarget()`. The scheduler executes the builder handler function with a given [target configuration](guide/glossary#target). + +The builder handler function can be synchronous (return a value) or asynchronous (return a Promise), or it can watch and return multiple values (return an Observable). +The return value or values must always be of type `BuilderOutput`. +This object contains a Boolean `success` field and an optional `error` field that can contain an error message. + +Angular provides some builders that are used by the CLI for commands such as `ng build`, `ng test`, and `ng lint`. +Default target configurations for these and other built-in CLI builders can be found (and customized) in the "architect" section of the [workspace configuration file](guide/workspace-config), `angular.json`. +You can also extend and customize Angular by creating your own builders, which you can run using the [`ng run` CLI command](cli/run). + +### Builder project structure + +A builder resides in a "project" folder that is similar in structure to an Angular workspace, with global configuration files at the top level, and more specific configuration in a source folder with the code files that define the behavior. +For example, your `myBuilder` folder could contain the following files. + +| FILES | PURPOSE | +| :----------------------- | :------------------------------------------| +| `src/my-builder.ts` | Main source file for the builder definition. | +| `src/my-builder.spec.ts` | Source file for tests. | +| `src/schema.json` | Definition of builder input options. | +| `builders.json` | Testing configuration. | +| `package.json` | Dependencies. See https://docs.npmjs.com/files/package.json. | +| `tsconfig.json` | [TypeScript configuration](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). | + +You can publish the builder to `npm` (see [Publishing your Library](https://angular.io/guide/creating-libraries#publishing-your-library)). If you publish it as `@example/my-builder`, you can install it using the following command. + + + +npm install @example/my-builder + + + +## Creating a builder + +As an example, let’s create a builder that executes a shell command. +To create a builder, use the `createBuilder()` CLI Builder function, and return a `BuilderOutput` object. + +``` + +import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; + +export default createBuilder(_commandBuilder); + +function _commandBuilder( + options: JsonObject, + context: BuilderContext, + ): Promise { + ... +} + +``` + +Now let’s add some logic to it. +The following code retrieves the command and arguments from the user options, spawns the new process, and waits for the process to finish. +If the process is successful (returns a code of 0), it resolves the return value. + +``` +import { BuilderOutput, createBuilder } from '@angular-devkit/architect'; +import * as childProcess from 'child_process'; + +export default createBuilder(_commandBuilder); + +function _commandBuilder( + options: JsonObject, + context: BuilderContext, +): Promise { + const child = childProcess.spawn(options.command, options.args); + return new Promise(resolve => { + child.on('close', code => { + resolve({success: code === 0}); + }); + }); +} +``` + +### Handling output + +By default, the `spawn()` method outputs everything to the process standard output and error. +To make it easier to test and debug, we can forward the output to the CLI Builder logger instead. +This also allows the builder itself to be executed in a separate process, even if the standard output and error are deactivated (as in an [Electron app](https://electronjs.org/)). + +We can retrieve a Logger instance from the context. + +``` +import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; +import * as childProcess from 'child_process'; + +export default createBuilder(_commandBuilder); + +function _commandBuilder( + options: JsonObject, + context: BuilderContext, +): Promise { + const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'}); + child.stdout.on('data', (data) => { + context.logger.info(data.toString()); + }); + child.stderr.on('data', (data) => { + context.logger.error(data.toString()); + }); + + return new Promise(resolve => { + child.on('close', code => { + resolve({success: code === 0}); + }); + }); +} + +``` + +### Progress and status reporting + +The CLI Builder API includes progress and status reporting tools, which can provide hints for certain functions and interfaces. + +To report progress, use the `BuilderContext.reportProgress()` method, which takes a current value, (optional) total, and status string as arguments. +The total can be any number; for example, if you know how many files you have to process, the total could be the number of files, and current should be the number processed so far. +The status string is unmodified unless you pass in a new string value. + +You can see an [example](https://github.com/angular/angular-cli/blob/ba21c855c0c8b778005df01d4851b5a2176edc6f/packages/angular_devkit/build_angular/src/tslint/index.ts#L107) of how the `tslint` builder reports progress. + +In our example, the shell command either finishes or is still executing, so there’s no need for a progress report, but we can report status so that a parent builder that called our builder would know what’s going on. +Use the `BuilderContext.reportStatus()` method to generate a status string of any length. +(Note that there’s no guarantee that a long string will be shown entirely; it could be cut to fit the UI that displays it.) +Pass an empty string to remove the status. + +``` +import { BuilderOutput, createBuilder, BuilderContext } from '@angular-devkit/architect'; +import * as childProcess from 'child_process'; + +export default createBuilder(_commandBuilder); + +function _commandBuilder( + options: JsonObject, + context: BuilderContext, +): Promise { + context.reportStatus(`Executing "${options.command}"...`); + const child = childProcess.spawn(options.command, options.args, {stdio: 'pipe'}); + + child.stdout.on('data', (data) => { + context.logger.info(data.toString()); + }); + child.stderr.on('data', (data) => { + context.logger.error(data.toString()); + }); + + return new Promise(resolve => { + context.reportStatus(`Done.`); + child.on('close', code => { + resolve({success: code === 0}); + }); + }); +} +``` + +## Builder input + +You can invoke a builder indirectly through a CLI command, or directly with the Angular CLI `ng run` command. +In either case, you must provide required inputs, but can allow other inputs to default to values that are pre-configured for a specific [*target*](guide/glossary#target), provide a pre-defined, named override configuration, and provide further override option values on the command line. + +### Input validation + +You define builder inputs in a JSON schema associated with that builder. +The Architect tool collects the resolved input values into an `options` object, and validates their types against the schema before passing them to the builder function. +(The Schematics library does the same kind of validation of user input). + +For our example builder, we expect the `options` value to be a `JsonObject` with two keys: a `command` that is a string, and an `args` array of string values. + +We can provide the following schema for type validation of these values. + + + +{ + "$schema": "http://json-schema.org/schema", + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } +} + + + +
+ +This is a very simple example, but the use of a schema for validation can be very powerful. +For more information, see the [JSON schemas website](http://json-schema.org/). + +
+ +To link our builder implementation with its schema and name, we need to create a *builder definition* file, which we can point to in `package.json`. + +Create a file named `builders.json` file that looks like this. + + + +{ + "builders": { + "command": { + "implementation": "./command", + "schema": "./command/schema.json", + "description": "Runs any command line in the operating system." + } + } +} + + + +In the `package.json` file, add a `builders` key that tells the Architect tool where to find our builder definition file. + + + +{ + "name": "@example/command-runner", + "version": "1.0.0", + "description": "Builder for Command Runner", + "builders": "builders.json", + "devDependencies": { + "@angular-devkit/architect": "^1.0.0" + } +} + + + +The official name of our builder is now ` @example/command-runner:command`. +The first part of this is the package name (resolved using node resolution), and the second part is the builder name (resolved using the `builder.json` file). + +Using one of our `options` is very straightforward, we did this in the previous section when we accessed `options.command`. + + + context.reportStatus(`Executing "${options.command}"...`); + const child = childProcess.spawn(options.command, options.args, { stdio: 'pipe' }); + + + +### Target configuration + +A builder must have a defined target that associates it with a specific input configuration and [project](guide/glossary#project). + +Targets are defined in the `angular.json` [workspace configuration file](guide/workspace-config). +A target specifies the builder to use, its default options configuration, and named alternative configurations. +The Architect tool uses the target definition to resolve input options for a given run. + +The `angular.json` file has a section for each project, and the "architect" section of each project configures targets for builders used by CLI commands such as 'build', 'test', and 'lint'. +By default, for example, the `build` command runs the builder `@angular-devkit/build-angular:browser` to perform the build task, and passes in default option values as specified for the `build` target in `angular.json`. + + +{ + "myApp": { + ... + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/myApp", + "index": "src/index.html", + ... + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + ... + } + } + }, + ... + + + +The command passes the builder the set of default options specified in the "options" section. +If you pass the `--configuration=production` flag, it uses the override values specified in the `production` alternative configuration. +You can specify further option overrides individually on the command line. +You might also add more alternative configurations to the `build` target, to define other environments such as `stage` or `qa`. + +#### Target strings + +The generic `ng run` CLI command takes as its first argument a target string of the form *project:target[:configuration]*. + +* *project*: The name of the Angular CLI project that the target is associated with. + +* *target*: A named builder configuration from the `architect` section of the `angular.json` file. + +* *configuration*: (optional) The name of a specific configuration override for the given target, as defined in the `angular.json` file. + +If your builder calls another builder, it may need to read a passed target string. +You can parse this string into an object by using the `targetFromTargetString()` utility function from `@angular-devkit/architect`. + +## Schedule and run + +Architect runs builders asynchronously. +To invoke a builder, you schedule a task to be run when all configuration resolution is complete. + +The builder function is not executed until the scheduler returns a `BuilderRun` control object. +The CLI typically schedules tasks by calling the `BuilderContext.scheduleTarget()` function, and then resolves input options using the target definition in the `angular.json` file. + +Architect resolves input options for a given target by taking the default options object, then overwriting values from the configuration used (if any), then further overwriting values from the overrides object passed to `BuilderContext.scheduleTarget()`. +For the Angular CLI, the overrides object is built from command line arguments. + +Architect validates the resulting options values against the schema of the builder. +If inputs are valid, Architect creates the context and executes the builder. + +For more information see [Workspace Configuration](guide/workspace-config). + +
+ + You can also invoke a builder directly from another builder or test by calling `BuilderContext.scheduleBuilder()`. + You pass an `options` object directly to the method, and those option values are validated against the schema of the builder without further adjustment. + + Only the `BuilderContext.scheduleTarget()` method resolves the configuration and overrides through the `angular.json` file. + +
+ +### Default architect configuration + +Let’s create a simple `angular.json` file that puts target configurations into context. + +We can publish the builder to npm (see [Publishing your Library](https://angular.io/guide/creating-libraries#publishing-your-library)), and install it using the following command: + + + +npm install @example/command-runner + + + +If we create a new project with `ng new builder-test`, the generated `angular.json` file looks something like this, with only default builder configurations. + + + +{ + // ... + "projects": { + // ... + "builder-test": { + // ... + "architect": { + // ... + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + // ... more options... + "outputPath": "dist/builder-test", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json" + }, + "configurations": { + "production": { + // ... more options... + "optimization": true, + "aot": true, + "buildOptimizer": true + } + } + } + } + } + } + // ... +} + + + +### Adding a target + +Let's add a new target that will run our builder to execute a particular command. +This target will tell the builder to run `touch` on a file, in order to update its modified date. + +We need to update the `angular.json` file to add a target for this builder to the "architect" section of our new project. + +* We'll add a new target section to the "architect" object for our project. + +* The target named "touch" uses our builder, which we published to `@example/command-runner`. (See [Publishing your Library](https://angular.io/guide/creating-libraries#publishing-your-library)) + +* The options object provides default values for the two inputs that we defined; `command`, which is the Unix command to execute, and `args`, an array that contains the file to operate on. + +* The configurations key is optional, we'll leave it out for now. + + + +{ + "projects": { + "builder-test": { + "architect": { + "builder-test": { + "touch": { + "builder": "@example/command-runner:command", + "options": { + "command": "touch", + "args": [ + "src/main.ts" + ] + } + } + }, + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/builder-test", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json" + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "aot": true, + "buildOptimizer": true + } + } + } + } + } + } +} + + + +### Running the builder + +To run our builder with the new target's default configuration, use the following CLI command in a Linux shell. + + + + ng run builder-test:touch + + + +This will run the `touch` command on the `src/main.ts` file. + +You can use command-line arguments to override the configured defaults. +For example, to run with a different `command` value, use the following CLI command. + + + +ng run builder-test:touch --command=ls + + + +This will call the `ls` command instead of the `touch` command. +Because we did not override the *args* option, it will list information about the `src/main.ts` file (the default value provided for the target). + +## Testing a builder + +Use integration testing for your builder, so that you can use the Architect scheduler to create a context, as in this [example](https://github.com/mgechev/cli-builders-demo). + +* In the builder source directory, we have created a new test file `index.spec.ts`. The code creates new instances of `JsonSchemaRegistry` (for schema validation), `TestingArchitectHost` (an in-memory implementation of `ArchitectHost`), and `Architect`. + +* We've added a `builders.json` file next to the builder's [`package.json` file](https://github.com/mgechev/cli-builders-demo/blob/master/command-builder/builders.json), and modified the package file to point to it. + +Here’s an example of a test that runs the command builder. +The test uses the builder to run the `ls` command, then validates that it ran successfully and listed the proper files. + + + +import { Architect, ArchitectHost } from '@angular-devkit/architect'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing'; +// Our builder forwards the STDOUT of the command to the logger. +import { logging, schema } from '@angular-devkit/core'; + +describe('Command Runner Builder', () => { + let architect: Architect; + let architectHost: ArchitectHost; + + beforeEach(async () => { + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + // TestingArchitectHost() takes workspace and current directories. + // Since we don't use those, both are the same in this case. + architectHost = new TestingArchitectHost(__dirname, __dirname); + architect = new Architect(architectHost, registry); + + // This will either take a Node package name, or a path to the directory + // for the package.json file. + await architectHost.addBuilderFromPackage('..'); + }); + + // This might not work in Windows. + it('can run ls', async () => { + // Create a logger that keeps an array of all messages that were logged. + const logger = new logging.Logger(''); + const logs = []; + logger.subscribe(ev => logs.push(ev.message)); + + // A "run" can have multiple outputs, and contains progress information. + const run = await architect.scheduleBuilder('@example/command-runner:command', { + command: 'ls', + args: [__dirname], + }, { logger }); // We pass the logger for checking later. + + // The "result" member (of type BuilderOutput) is the next output. + const output = await run.result; + + // Stop the builder from running. This stops Architect from keeping + // the builder-associated states in memory, since builders keep waiting + // to be scheduled. + await run.stop(); + + // Expect that it succeeded. + expect(output.success).toBe(true); + + // Expect that this file was listed. It should be since we're running + // `ls $__dirname`. + expect(logs).toContain('index.spec.ts'); + }); +}); + + + +
+ + When running this test in your repo, you need the [`ts-node`](https://github.com/TypeStrong/ts-node) package. You can avoid this by renaming `index.spec.ts` to `index.spec.js`. + +
+ +### Watch mode + +Architect expects builders to run once (by default) and return. +This behavior is not entirely compatible with a builder that watches for changes (like Webpack, for example). +Architect can support watch mode, but there are some things to look out for. + +* To be used with watch mode, a builder handler function should return an Observable. Architect subscribes to the Observable until it completes and might reuse it if the builder is scheduled again with the same arguments. + +* The builder should always emit a `BuilderOutput` object after each execution. Once it’s been executed, it can enter a watch mode, to be triggered by an external event. If an event triggers it to restart, the builder should execute the `BuilderContext.reportRunning()` function to tell Architect that it is running again. This prevents Architect from stopping the builder if another run is scheduled. + +When your builder calls `BuilderRun.stop()` to exit watch mode, Architect unsubscribes from the builder’s Observable and calls the builder’s teardown logic to clean up. +(This behavior also allows for long running builds to be stopped and cleaned up.) + +In general, if your builder is watching an external event, you should separate your run into three phases. + +1. **Running** + For example, webpack compiles. This ends when webpack finishes and your builder emits a `BuilderOutput` object. + +1. **Watching** + Between two runs, watch an external event stream. For example, webpack watches the file system for any changes. This ends when webpack restarts building, and `BuilderContext.reportRunning()` is called. This goes back to step 1. + +1. **Completion** + Either the task is fully completed (for example, webpack was supposed to run a number of times), or the builder run was stopped (using `BuilderRun.stop()`). Your teardown logic is executed, and Architect unsubscribes from your builder’s Observable. + +## Summary + +The CLI Builder API provides a new way of changing the behavior of the Angular CLI by using builders to execute custom logic. + +* Builders can be synchronous or asynchronous, execute once or watch for external events, and can schedule other builders or targets. + +* Builders have option defaults specified in the `angular.json` configuration file, which can be overwritten by an alternate configuration for the target, and further overwritten by command line flags. + +* We recommend that you use integration tests to test Architect builders. You can use unit tests to validate the logic that the builder executes. + +* If your builder returns an Observable, it should clean up in the teardown logic of that Observable. \ No newline at end of file diff --git a/aio/content/guide/component-styles.md b/aio/content/guide/component-styles.md index 85c9df1e09..27885a6289 100644 --- a/aio/content/guide/component-styles.md +++ b/aio/content/guide/component-styles.md @@ -166,12 +166,11 @@ Component styles normally apply only to the HTML in the component's own template 组件样式通常只会作用于组件自身的 HTML 上。 -Use the `/deep/` shadow-piercing descendant combinator to force a style down through the child -component tree into all the child component views. -The `/deep/` combinator works to any depth of nested components, and it applies to both the view -children and content children of the component. - -可以使用 `/deep/` 选择器来强制一个样式对各级子组件的视图也生效,它不但会作用于组件的子视图,也会作用于投影进来的内容(`ng-content`)。 +Applying the `::ng-deep` pseudo-class to any CSS rule completely disables view-encapsulation for +that rule. Any style with `::ng-deep` applied becomes a global style. In order to scope the specified style +to the current component and all its descendants, be sure to include the `:host` selector before +`::ng-deep`. If the `::ng-deep` combinator is used without the `:host` pseudo-class selector, the style +can bleed into other components. The following example targets all `

` elements, from the host element down through this component to all of its child elements in the DOM. diff --git a/aio/content/guide/dependency-injection.md b/aio/content/guide/dependency-injection.md index 71b1c7d4d7..e547a49fbd 100644 --- a/aio/content/guide/dependency-injection.md +++ b/aio/content/guide/dependency-injection.md @@ -1,16 +1,16 @@ -# Dependency Injection in Angular +# Dependency Injection in Angular # Angular 中的依赖注入 Dependency injection (DI), is an important application design pattern. -Angular has its own DI framework, which is typically +Angular has its own DI framework, which is typically used in the design of Angular applications to increase their efficiency and modularity. 依赖注入(DI)是一种重要的应用设计模式。 Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。 Dependencies are services or objects that a class needs to perform its function. -DI is a coding pattern in which a class asks for dependencies from external sources rather than creating them itself. +DI is a coding pattern in which a class asks for dependencies from external sources rather than creating them itself. 依赖,是当类需要执行其功能时,所需要的服务或对象。 DI 是一种编码模式,其中的类会从外部源中请求获取依赖,而不是自己创建它们。 @@ -85,7 +85,7 @@ Having multiple classes in the same file can be confusing. We generally recommen 在同一个文件中放多个类容易让人困惑。我们通常建议你在单独的文件中定义组件和服务。 If you do combine a component and service in the same file, -it is important to define the service first, and then the component. If you define the component before the service, you get a run-time null reference error. +it is important to define the service first, and then the component. If you define the component before the service, you get a run-time null reference error. 如果你把组件和服务都放在同一个文件中,请务必先定义服务,然后再定义组件。如果在服务之前定义组件,则会在运行时收到一个空引用错误。 @@ -136,24 +136,24 @@ The `@Injectable()` is an essential ingredient in every Angular service definiti The class we have created provides a service. The `@Injectable()` decorator marks it as a service that can be injected, but Angular can't actually inject it anywhere until you configure -an Angular [dependency injector](guide/glossary#injector) with a [provider](guide/glossary#provider) of that service. +an Angular [dependency injector](guide/glossary#injector) with a [provider](guide/glossary#provider) of that service. 我们创建的类提供了一个服务。`@Injectable()` 装饰器把它标记为可供注入的服务,不过在你使用该服务的 [provider](guide/glossary#provider) 提供商配置好 Angular 的[依赖注入器](guide/glossary#injector)之前,Angular 实际上无法将其注入到任何位置。 -The injector is responsible for creating service instances and injecting them into classes like `HeroListComponent`. +The injector is responsible for creating service instances and injecting them into classes like `HeroListComponent`. You rarely create an Angular injector yourself. Angular creates injectors for you as it executes the app, starting with the _root injector_ that it creates during the [bootstrap process](guide/bootstrapping). 该注入器负责创建服务实例,并把它们注入到像 `HeroListComponent` 这样的类中。 你很少需要自己创建 Angular 的注入器。Angular 会在执行应用时为你创建注入器,第一个注入器是*根注入器*,创建于[启动过程](guide/bootstrapping)中。 -A provider tells an injector _how to create the service_. -You must configure an injector with a provider before that injector can create a service (or provide any other kind of dependency). +A provider tells an injector _how to create the service_. +You must configure an injector with a provider before that injector can create a service (or provide any other kind of dependency). 提供商会告诉注入器*如何创建该服务*。 要想让注入器能够创建服务(或提供其它类型的依赖),你必须使用某个提供商配置好注入器。 A provider can be the service class itself, so that the injector can use `new` to create an instance. -You might also define more than one class to provide the same service in different ways, +You might also define more than one class to provide the same service in different ways, and configure different injectors with different providers. 提供商可以是服务类本身,因此注入器可以使用 `new` 来创建实例。 @@ -162,10 +162,10 @@ and configure different injectors with different providers.
Injectors are inherited, which means that if a given injector can't resolve a dependency, -it asks the parent injector to resolve it. -A component can get services from its own injector, -from the injectors of its component ancestors, -from the injector of its parent NgModule, or from the `root` injector. +it asks the parent injector to resolve it. +A component can get services from its own injector, +from the injectors of its component ancestors, +from the injector of its parent NgModule, or from the `root` injector. 注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它。 组件可以从它自己的注入器来获取服务、从其祖先组件的注入器中获取、从其父 NgModule 的注入器中获取,或从 `root` 注入器中获取。 @@ -192,7 +192,7 @@ You can configure injectors with providers at different levels of your app, by s 在 NgModule 的 `@NgModule()` 装饰器中。 -* In the `@Component()` decorator for a component. +* In the `@Component()` decorator for a component. 在组件的 `@Component()` 装饰器中。 @@ -217,14 +217,14 @@ Learn more about [where to configure providers](guide/hierarchical-dependency-in
-{@a injector-config} +{@a injector-config} {@a bootstrap} ## Injecting services ## 注入服务 -In order for `HeroListComponent` to get heroes from `HeroService`, it needs to ask for `HeroService` to be injected, rather than creating it's own `HeroService` instance with `new`. +In order for `HeroListComponent` to get heroes from `HeroService`, it needs to ask for `HeroService` to be injected, rather than creating its own `HeroService` instance with `new`. `HeroListComponent` 要想从 `HeroService` 中获取英雄列表,就得要求注入 `HeroService`,而不是自己使用 `new` 来创建自己的 `HeroService` 实例。 @@ -263,7 +263,7 @@ If you decided to provide `HeroService` in `AppModule`, `HeroListComponent` woul ### 注入器树与服务实例 -Services are singletons _within the scope of an injector_. That is, there is at most one instance of a service in a given injector. +Services are singletons _within the scope of an injector_. That is, there is at most one instance of a service in a given injector. *在某个注入器*的范围内,服务是单例的。也就是说,在指定的注入器中最多只有某个服务的最多一个实例。 @@ -272,15 +272,15 @@ There is only one root injector for an app. Providing `UserService` at the `root 应用只有一个根注入器。在 `root` 或 `AppModule` 级提供 `UserService` 意味着它注册到了根注入器上。 在整个应用中只有一个 `UserService` 实例,每个要求注入 `UserService` 的类都会得到这一个服务实例,*除非*你在*子注入器*中配置了另一个提供商。 -Angular DI has a [hierarchical injection system](guide/hierarchical-dependency-injection), which means that nested injectors can create their own service instances. +Angular DI has a [hierarchical injection system](guide/hierarchical-dependency-injection), which means that nested injectors can create their own service instances. Angular regularly creates nested injectors. Whenever Angular creates a new instance of a component that has `providers` specified in `@Component()`, it also creates a new _child injector_ for that instance. -Similarly, when a new NgModule is lazy-loaded at run time, Angular can create an injector for it with its own providers. +Similarly, when a new NgModule is lazy-loaded at run time, Angular can create an injector for it with its own providers. Angular DI 具有[分层注入体系](guide/hierarchical-dependency-injection),这意味着下级注入器也可以创建它们自己的服务实例。 Angular 会有规律的创建下级注入器。每当 Angular 创建一个在 `@Component()` 中指定了 `providers` 的组件实例时,它也会为该实例创建一个新的*子注入器*。 类似的,当在运行期间加载一个新的 NgModule 时,Angular 也可以为它创建一个拥有自己的提供商的注入器。 -Child modules and component injectors are independent of each other, and create their own separate instances of the provided services. When Angular destroys an NgModule or component instance, it also destroys that injector and that injector's service instances. +Child modules and component injectors are independent of each other, and create their own separate instances of the provided services. When Angular destroys an NgModule or component instance, it also destroys that injector and that injector's service instances. 子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的实例。当 Angular 销毁 NgModule 或组件实例时,也会销毁这些注入器以及注入器中的那些服务实例。 @@ -326,7 +326,7 @@ Learn more in the [Testing](guide/testing) guide. {@a service-needs-service} -## Services that need other services +## Services that need other services ## 那些需要其它服务的服务 @@ -363,7 +363,7 @@ Notice that the `Logger` service also has the `@Injectable()` decorator, even th 注意,虽然 `Logger` 服务没有自己的依赖项,但是它同样带有 `@Injectable()` 装饰器。实际上,`@Injectable()` **对所有服务都是必须的**。 When Angular creates a class whose constructor has parameters, it looks for type and injection metadata about those parameters so that it can inject the correct service. -If Angular can't find that parameter information, it throws an error. +If Angular can't find that parameter information, it throws an error. Angular can only find the parameter information _if the class has a decorator of some kind_. The `@Injectable()` decorator is the standard decorator for service classes. @@ -377,7 +377,6 @@ The `@Injectable()` decorator is the standard decorator for service classes. The decorator requirement is imposed by TypeScript. TypeScript normally discards parameter type information when it [transpiles](guide/glossary#transpile) the code to JavaScript. TypeScript preserves this information if the class has a decorator and the `emitDecoratorMetadata` compiler option is set `true` in TypeScript's `tsconfig.json` configuration file. The CLI configures `tsconfig.json` with `emitDecoratorMetadata: true`. 对装饰器的需求是 TypeScript 强制要求的。当 TypeScript 把代码[转译](guide/glossary#transpile)成 JavaScript 时,一般会丢弃参数的类型信息。只有当类具有装饰器,并且 `tsconfig.json` 中的编译器选项 `emitDecoratorMetadata` 为 `true` 时,TypeScript 才会保留这些信息。CLI 所配置的 `tsconfig.json` 就带有 `emitDecoratorMetadata: true`。 - This means you're responsible for putting `@Injectable()` on your service classes. 这意味着你有责任给所有服务类加上 `@Injectable()`。 @@ -419,7 +418,7 @@ Angular knows to inject the service associated with that `HeroService` class tok -Many dependency values are provided by classes, but not all. The expanded *provide* object lets you associate different kinds of providers with a DI token. +Many dependency values are provided by classes, but not all. The expanded *provide* object lets you associate different kinds of providers with a DI token. 很多依赖项的值都是通过类来提供的,但不是全部。扩展的 *provide* 对象让你可以把多种不同种类的提供商和 DI 令牌关联起来。 @@ -438,7 +437,7 @@ one? `HeroService` *需要*一个记录器,但是如果找不到它会怎么样? -When a component or service declares a dependency, the class constructor takes that dependency as a parameter. +When a component or service declares a dependency, the class constructor takes that dependency as a parameter. You can tell Angular that the dependency is optional by annotating the constructor parameter with `@Optional()`. diff --git a/aio/content/guide/deployment.md b/aio/content/guide/deployment.md index 5908a574b7..6e3f06e563 100644 --- a/aio/content/guide/deployment.md +++ b/aio/content/guide/deployment.md @@ -536,12 +536,12 @@ showing exactly which classes are included in the bundle. `source-map-explorer` 会分析与包一起生成的 source map,并画出所有依赖的地图,精确展示哪些类包含在哪个包中。 -Here's the output for the _main_ bundle of the QuickStart. +Here's the output for the _main_ bundle of an example app called `cli-quickstart`. 下面是 "快速上手" 应用中 `main` 包的输出。
- quickstart sourcemap explorer + quickstart sourcemap explorer
{@a base-tag} diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index 4aa0d75076..a0d4d321bf 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -371,8 +371,6 @@ const routes: Routes = [{ | ------------- | --------------- | | 属性 | 替代品 | | `params` | `paramMap` | -| `params` | `paramMap` | -| `queryParams` | `queryParamMap` | | `queryParams` | `queryParamMap` | For more information see the [Router guide](guide/router#activated-route). @@ -452,66 +450,36 @@ For more information about using `@angular/common/http`, see the [HttpClient gui | --------------------- | ----------------------------------------------------------------- | | `@angular/http` | `@angular/common/http` 中最接近的替代品 | | `BaseRequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) | -| `BaseRequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) | -| `BaseResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) | | `BaseResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) | | `BrowserXhr` | | -| `BrowserXhr` | | -| `Connection` | [`HttpBackend`](/api/common/http/HttpBackend) | | `Connection` | [`HttpBackend`](/api/common/http/HttpBackend) | | `ConnectionBackend` | [`HttpBackend`](/api/common/http/HttpBackend) | -| `ConnectionBackend` | [`HttpBackend`](/api/common/http/HttpBackend) | -| `CookieXSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) | | `CookieXSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) | | `Headers` | [`HttpHeaders`](/api/common/http/HttpHeaders) | -| `Headers` | [`HttpHeaders`](/api/common/http/HttpHeaders) | -| `Http` | [`HttpClient`](/api/common/http/HttpClient) | | `Http` | [`HttpClient`](/api/common/http/HttpClient) | | `HttpModule` | [`HttpClientModule`](/api/common/http/HttpClientModule) | -| `HttpModule` | [`HttpClientModule`](/api/common/http/HttpClientModule) | -| `Jsonp` | [`HttpClient`](/api/common/http/HttpClient) | | `Jsonp` | [`HttpClient`](/api/common/http/HttpClient) | | `JSONPBackend` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) | -| `JSONPBackend` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) | -| `JSONPConnection` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) | | `JSONPConnection` | [`JsonpClientBackend`](/api/common/http/JsonpClientBackend) | | `JsonpModule` | [`HttpClientJsonpModule`](/api/common/http/HttpClientJsonpModule) | -| `JsonpModule` | [`HttpClientJsonpModule`](/api/common/http/HttpClientJsonpModule) | -| `QueryEncoder` | [`HttpUrlEncodingCodec`](/api/common/http/HttpUrlEncodingCodec) | | `QueryEncoder` | [`HttpUrlEncodingCodec`](/api/common/http/HttpUrlEncodingCodec) | | `ReadyState` | [`HttpBackend`](/api/common/http/HttpBackend) | -| `ReadyState` | [`HttpBackend`](/api/common/http/HttpBackend) | -| `Request` | [`HttpRequest`](/api/common/http/HttpRequest) | | `Request` | [`HttpRequest`](/api/common/http/HttpRequest) | | `RequestMethod` | [`HttpClient`](/api/common/http/HttpClient) | -| `RequestMethod` | [`HttpClient`](/api/common/http/HttpClient) | -| `RequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) | | `RequestOptions` | [`HttpRequest`](/api/common/http/HttpRequest) | | `RequestOptionsArgs` | [`HttpRequest`](/api/common/http/HttpRequest) | -| `RequestOptionsArgs` | [`HttpRequest`](/api/common/http/HttpRequest) | -| `Response` | [`HttpResponse`](/api/common/http/HttpResponse) | | `Response` | [`HttpResponse`](/api/common/http/HttpResponse) | | `ResponseContentType` | [`HttpClient`](/api/common/http/HttpClient) | -| `ResponseContentType` | [`HttpClient`](/api/common/http/HttpClient) | -| `ResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) | | `ResponseOptions` | [`HttpResponse`](/api/common/http/HttpResponse) | | `ResponseOptionsArgs` | [`HttpResponse`](/api/common/http/HttpResponse) | -| `ResponseOptionsArgs` | [`HttpResponse`](/api/common/http/HttpResponse) | -| `ResponseType` | [`HttpClient`](/api/common/http/HttpClient) | | `ResponseType` | [`HttpClient`](/api/common/http/HttpClient) | | `URLSearchParams` | [`HttpParams`](/api/common/http/HttpParams) | -| `URLSearchParams` | [`HttpParams`](/api/common/http/HttpParams) | -| `XHRBackend` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) | | `XHRBackend` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) | | `XHRConnection` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) | -| `XHRConnection` | [`HttpXhrBackend`](/api/common/http/HttpXhrBackend) | -| `XSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) | | `XSRFStrategy` | [`HttpClientXsrfModule`](/api/common/http/HttpClientXsrfModule) | | `@angular/http/testing` | Closest replacement in `@angular/common/http/testing` | | ----------------------- | ------------------------------------------------------------------------- | | `@angular/http/testing` | `@angular/common/http/testing` 中最接近的替代品 | | `MockBackend` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) | -| `MockBackend` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) | -| `MockConnection` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) | | `MockConnection` | [`HttpTestingController`](/api/common/http/testing/HttpTestingController) | diff --git a/aio/content/guide/displaying-data.md b/aio/content/guide/displaying-data.md index ef153d5179..e26a124e40 100644 --- a/aio/content/guide/displaying-data.md +++ b/aio/content/guide/displaying-data.md @@ -43,10 +43,9 @@ With interpolation, you put the property name in the view template, enclosed in 要显示组件的属性,最简单的方式就是通过插值表达式 (interpolation) 来绑定属性名。 要使用插值表达式,就把属性名包裹在双花括号里放进视图模板,如 `{{myHero}}`。 -Follow the [Getting Started](guide/quickstart) instructions for creating a new project -named displaying-data. +Use the CLI command [`ng new displaying-data`](cli/new) to create a workspace and app named `displaying-data`. -按照[快速起步](guide/quickstart)的说明,创建一个新项目,名为displaying-data。 +使用 CLI 命令 [`ng new displaying-data`](cli/new) 创建一个工作空间和一个名叫 `displaying-data` 的应用。 Delete the app.component.html file. It is not needed for this example. diff --git a/aio/content/guide/elements.md b/aio/content/guide/elements.md index a0213a1a7d..7d9c8cb85c 100644 --- a/aio/content/guide/elements.md +++ b/aio/content/guide/elements.md @@ -2,13 +2,13 @@ # Angular 元素(Elements)概览 -_Angular elements_ are Angular components packaged as _custom elements_, a web standard for defining new HTML elements in a framework-agnostic way. +_Angular elements_ are Angular components packaged as _custom elements_ (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way. *Angular 元素*就是打包成*自定义元素*的 Angular 组件。所谓自定义元素就是一套与具体框架无关的用于定义新 HTML 元素的 Web 标准。 [Custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) are a Web Platform feature currently supported by Chrome, Firefox, Opera, and Safari, and available in other browsers through polyfills (see [Browser Support](#browser-support)). A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code. -The browser maintains a `CustomElementRegistry` of defined custom elements (also called Web Components), which maps an instantiable JavaScript class to an HTML tag. +The browser maintains a `CustomElementRegistry` of defined custom elements, which maps an instantiable JavaScript class to an HTML tag. [自定义元素](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)这项特性目前受到了 Chrome、Opera 和 Safari 的支持,在其它浏览器中也能通过腻子脚本(参见[浏览器支持](#browser-support))加以支持。 自定义元素扩展了 HTML,它允许你定义一个由 JavaScript 代码创建和控制的标签。 diff --git a/aio/content/guide/file-structure.md b/aio/content/guide/file-structure.md index cbaf31607f..d6a8e8d067 100644 --- a/aio/content/guide/file-structure.md +++ b/aio/content/guide/file-structure.md @@ -14,18 +14,18 @@ Angular CLI 的 `ng new`命令可以创建一个工作空间。 -ng new <project_name> +ng new <my-project> -When you run this command, the CLI installs the necessary Angular npm packages and other dependencies in a new workspace, with a root folder named *project_name*. -The workspace root folder contains workspace configuration files and a README file with generated descriptive text that you can customize. +When you run this command, the CLI installs the necessary Angular npm packages and other dependencies in a new workspace, with a root-level application named *my-project*. +The workspace root folder contains various support and configuration files, and a README file with generated descriptive text that you can customize. 当你运行这个命令时,CLI 会在一个新的工作区中安装必需的 Angular npm 包和其它依赖项,其根文件夹名叫 *project_name*。该工作空间的根文件夹中包含一些工作空间配置文件,和一个带有自动生成的描述性文本的自述文件,你可以自定义它。 -By default, `ng new` also creates an initial skeleton application, along with its end-to-end tests. +By default, `ng new` creates an initial skeleton application at the root level of the workspace, along with its end-to-end tests. The skeleton is for a simple Welcome application that is ready to run and easy to modify. -This *root application* has the same name as the workspace, and the source files reside in the `src/` subfolder of the workspace. +The root-level application has the same name as the workspace, and the source files reside in the `src/` subfolder of the workspace. `ng new` 还会默认创建一个初始的骨架应用,以及它的端到端测试项目。这个骨架是一个简单的 Welcome 应用,它可以运行,也很容易修改。这个*根应用*与工作空间同名,其源文件位于工作空间的 `src/` 子文件夹中。 @@ -55,46 +55,47 @@ See [Setting up for a multi-project workspace](#multiple-projects) below. All projects within a workspace share a [CLI configuration context](guide/workspace-config). -The top level of the workspace contains workspace-wide configuration files. +The top level of the workspace contains workspace-wide configuration files, configuration files for the root-level application, and subfolders for the root-level application source and test files. 每个工作空间中的所有项目共享同一个 [CLI 配置环境](guide/workspace-config) 。该工作空间的顶层包含着全工作空间级的配置文件。 - -| WORKSPACE CONFIG FILES | PURPOSE | -| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| 工作空间配置文件 | 用途 | -| `node_modules/` | Provides [npm packages](guide/npm-packages) to the entire workspace. Workspace-wide `node_modules` dependencies are visible to all projects. | -| `node_modules/` | 为整个工作空间提供了 [npm 包](guide/npm-packages) 。所有项目都可以看到工作区范围内的 `node_modules` 依赖项。 | -| `.editorconfig` | Configuration for code editors. See [EditorConfig](https://editorconfig.org/). | -| `.editorconfig` | 代码编辑器的配置。参见 [EditorConfig](https://editorconfig.org/) 。 | -| `.gitignore` | Specifies intentionally untracked files that [Git](https://git-scm.com/) should ignore. | -| `.gitignore` | 指定 [Git](https://git-scm.com/) 应忽略的不必追踪的文件。 | +| WORKSPACE CONFIG FILES | PURPOSE | +| :--------------------- | :------------------------------------------| +| 工作空间配置文件 | 用途 | +| `.editorconfig` | Configuration for code editors. See [EditorConfig](https://editorconfig.org/). | +| `.editorconfig` | 代码编辑器的配置。参见 [EditorConfig](https://editorconfig.org/) 。 | +| `.gitignore` | Specifies intentionally untracked files that [Git](https://git-scm.com/) should ignore. | +| `.gitignore` | 指定 [Git](https://git-scm.com/) 应忽略的不必追踪的文件。 | +| `README.md` | Introductory documentation for the root app. | +| `README.md` | 根应用的简介文档. | | `angular.json` | CLI configuration defaults for all projects in the workspace, including configuration options for build, serve, and test tools that the CLI uses, such as [TSLint](https://palantir.github.io/tslint/), [Karma](https://karma-runner.github.io/), and [Protractor](http://www.protractortest.org/). For details, see [Angular Workspace Configuration](guide/workspace-config). | | `angular.json` | 为工作区中的所有项目指定 CLI 的默认配置,包括 CLI 要用到的构建、启动开发服务器和测试工具的配置项,比如 [TSLint](https://palantir.github.io/tslint/),[Karma](https://karma-runner.github.io/) 和 [Protractor](http://www.protractortest.org/)。欲知详情,请参阅 [Angular 工作空间配置](guide/workspace-config) 部分。 | | `package.json` | Configures [npm package dependencies](guide/npm-packages) that are available to all projects in the workspace. See [npm documentation](https://docs.npmjs.com/files/package.json) for the specific format and contents of this file. | | `package.json` | 配置工作空间中所有项目可用的 [npm包依赖](guide/npm-packages) 。有关此文件的具体格式和内容,请参阅 [npm 的文档](https://docs.npmjs.com/files/package.json) 。 | | `package-lock.json` | Provides version information for all packages installed into `node_modules` by the npm client. See [npm documentation](https://docs.npmjs.com/files/package-lock.json) for details. If you use the yarn client, this file will be [yarn.lock](https://yarnpkg.com/lang/en/docs/yarn-lock/) instead. | | `package-lock.json` | 提供 npm 客户端安装到 `node_modules` 的所有软件包的版本信息。欲知详情,请参阅 [npm 的文档](https://docs.npmjs.com/files/package-lock.json)。如果你使用的是 yarn 客户端,那么该文件[就是 yarn.lock](https://yarnpkg.com/lang/en/docs/yarn-lock/) 。 | -| `README.md` | Introductory documentation for the root app. | +| `src/` | Source files for the root-level application project. | +| `node_modules/` | Provides [npm packages](guide/npm-packages) to the entire workspace. Workspace-wide `node_modules` dependencies are visible to all projects. | | `README.md` | 根应用的介绍性文档。 | | `tsconfig.json` | Default [TypeScript](https://www.typescriptlang.org/) configuration for projects in the workspace. | | `tsconfig.json` | 工作空间中各个项目的默认 [TypeScript](https://www.typescriptlang.org/) 配置。 | | `tslint.json` | Default [TSLint](https://palantir.github.io/tslint/) configuration for projects in the workspace. | | `tslint.json` | 工作空间中各个项目的默认 [TSLint](https://palantir.github.io/tslint/) 配置。 | + ## Application project files ## 应用项目文件 -By default, the CLI command `ng new my-app` creates a workspace folder named "my-app" and generates a new application skeleton for a root application at the top level of the workspace. +By default, the CLI command `ng new my-app` creates a workspace folder named "my-app" and generates a new application skeleton in a `src/` folder at the top level of the workspace. A newly generated application contains source files for a root module, with a root component and template. CLI 命令`ng new my-app` 会默认创建名为 “my-app” 的工作空间文件夹,并为工作空间顶层的根应用生成一个新的应用骨架。新生成的应用包含一个根模块的源文件,包括一个根组件及其模板。 When the workspace file structure is in place, you can use the `ng generate` command on the command line to add functionality and data to the application. -This initial starter application is the *default app* for CLI commands (unless you change the default after creating [additional apps](#multiple-projects)). +This initial root-level application is the *default app* for CLI commands (unless you change the default after creating [additional apps](#multiple-projects)). 当工作空间文件结构到位时,可以在命令行中使用 `ng generate` 命令往该应用中添加功能和数据。这个初始的起步者应用是 CLI 命令的*默认应用*(除非你在创建[其它应用](#multiple-projects)之后更改了默认值)。 @@ -204,13 +205,14 @@ Project-specific [TypeScript](https://www.typescriptlang.org/) configuration fil ### 端到端测试文件 -An `e2e/` subfolder contains source files for a set of end-to-end tests that correspond to an application, along with test-specific configuration files. +An `e2e/` folder at the top level contains source files for a set of end-to-end tests that correspond to the root-level application, along with test-specific configuration files. + +For a multi-project workspace, application-specific end-to-end tests are in the project root, under `projects/project-name/e2e/`. `e2e/` 子文件夹包含一组和应用对应的端到端测试的源文件,以及测试专属的配置文件。 -my-app/ e2e/ src/ (end-to-end tests for my-app) app.e2e-spec.ts @@ -237,7 +239,7 @@ A multi-project workspace is suitable for an enterprise that uses a single repos If you intend to have multiple projects in a workspace, you can skip the initial application generation when you create the workspace, and give the workspace a unique name. -The following command creates a workspace with all of the workspace-wide configuration files, but no root application. +The following command creates a workspace with all of the workspace-wide configuration files, but no root-level application. 如果你打算在工作区中包含多个项目,可以在创建工作空间时不要自动创建初始应用,并为工作空间指定一个唯一的名字。下列命令用于创建一个包含全工作空间级配置文件的工作空间,但没有根应用。 diff --git a/aio/content/guide/glossary.md b/aio/content/guide/glossary.md index 4e750f61a5..83465f9191 100644 --- a/aio/content/guide/glossary.md +++ b/aio/content/guide/glossary.md @@ -300,10 +300,9 @@ The [Angular CLI](cli) is a command-line tool for managing the Angular developme [Angular CLI](cli) 是一个命令行工具,用于管理 Angular 的开发周期。它用于为[工作区](#workspace)或[项目](#project)创建初始的脚手架,并且运行[生成器(schematics)](#schematic)来为初始生成的版本添加或修改各类代码。 CLI 支持开发周期中的所有阶段,比如构建、测试、打包和部署。 -* To begin using the CLI for a new project, see [Getting Started](guide/quickstart). +* To begin using the CLI for a new project, see [Local Environment Setup](guide/setup-local "Setting up for Local Development"). 要开始使用 CLI 来创建新项目,参见[快速起步](guide/quickstart)。 - * To learn more about the full capabilities of the CLI, see the [CLI command reference](cli). 要了解 CLI 的全部功能,参见 [CLI 命令参考手册](cli)。 @@ -646,15 +645,14 @@ Compare to [custom element](#custom-element). ## 入口点(Entry Point) -A JavaScript symbol that makes parts of an [npm package](guide/npm-packages) available for import by other code. -The Angular [scoped packages](#scoped-package) each have an entry point named `index`. +A JavaScript module(#module) that is intended to be imported by a user of [an +npm package](guide/npm-packages). An entry-point module typically re-exports +symbols from other internal modules. A package can contain multiple +entry points. For example, the `@angular/core` package has two entry-point +modules, which can be imported using the module names `@angular/core` and +`@angular/core/testing`. -JavaScript 的 ID 用来让这段代码成为 [npm 包](guide/npm-packages)的一部分,从而让其它代码能导入它。 -Angular 的每个[范围化的包](#scoped-package)都有一个名叫 `index` 的入口点。 - -Within Angular, use [NgModules](#ngmodule) to make public parts available for import by other NgModules. - -在 Angular 领域中,[NgModules](#ngmodule) 可以让一些公开的部分可以供其它 NgModules 导入。 +JavaScript 模块(#module)的目的是供 [npm 包](guide/npm-packages)的用户进行导入。入口点模块通常会重新导出来自其它内部模块的一些符号。每个包可以包含多个入口点。比如 `@angular/core` 就有两个入口点模块,它们可以使用名字 `@angular/core` 和 `@angular/core/testing` 进行导入。 {@a F} diff --git a/aio/content/guide/ivy.md b/aio/content/guide/ivy.md index 5ab5c74ae0..df7d26232e 100644 --- a/aio/content/guide/ivy.md +++ b/aio/content/guide/ivy.md @@ -2,7 +2,13 @@ # 选用 Angular Ivy ['aɪvɪ](常春藤) -Ivy is the code name for Angular's [next-generation compilation and rendering pipeline](https://blog.angular.io/a-plan-for-version-8-0-and-ivy-b3318dfc19f7). Starting with Angular version 8, you can choose to opt in to start using Ivy now, and help in its continuing develpment and tuning. +Ivy is the code name for Angular's [next-generation compilation and rendering pipeline](https://blog.angular.io/a-plan-for-version-8-0-and-ivy-b3318dfc19f7). Starting with Angular version 8, you can choose to opt in to start using a preview version of Ivy and help in its continuing development and tuning. + +
+ + To preview Ivy, use `@angular/core@next` version of Angular (8.1.x), rather than `@angular/core@latest` (8.0.x), as it contains all the latest bug fixes and improvements. + +
Ivy 是 Angular [下一代编译和渲染管道](https://blog.angular.io/a-plan-for-version-8-0-and-ivy-b3318dfc19f7)的代号。从 Angular 的版本 8 开始,你就可以开始选用 Ivy 了,以帮助它继续开发和调优。 @@ -45,7 +51,27 @@ To update an existing project to use Ivy, set the `enableIvy` option in the `ang } ``` -To stop using the Ivy compiler, set `enableIvy` to `false` in `tsconfig.app.json`, or remove it completely. +AOT compilation with Ivy is faster and should be used by default. In the `angular.json` workspace configuration file, set the default build options for your project to always use AOT compilation. + +```json +{ + "projects": { + "my-existing-project": { + "architect": { + "build": { + "options": { + ... + "aot": true, + } + } + } + } + } +} +``` + +To stop using the Ivy compiler, set `enableIvy` to `false` in `tsconfig.app.json`, or remove it completely. Also remove `"aot": true` from your default build options if you didn't have it there before. + 要停止使用 Ivy 编译器,请在 `enableIvy` 中把 `tsconfig.app.json` 设置为 `false` ,或者把它完全删除。 diff --git a/aio/content/guide/lazy-loading-ngmodules.md b/aio/content/guide/lazy-loading-ngmodules.md index 9c9c44703e..90aed15969 100644 --- a/aio/content/guide/lazy-loading-ngmodules.md +++ b/aio/content/guide/lazy-loading-ngmodules.md @@ -241,7 +241,7 @@ In `AppRoutingModule`, update the `routes` array with the following:
-The import statements stay the same. The first two paths are the routes to the `CustomersModule` and the `OrdersModule` respectively. Notice that the lazy loading syntax uses `loadChildren` followed by a string that is the relative path to the module, a hash mark or `#`, and the module’s class name. +The import statements stay the same. The first two paths are the routes to the `CustomersModule` and the `OrdersModule` respectively. Notice that the lazy loading syntax uses `loadChildren` followed by a function that uses the browser's built-in `import('...')` syntax for dynamic imports. The import path is the relative path to the module. 这些 `import` 语句没有变化。前两个路径分别路由到了 `CustomersModule` 和 `OrdersModule`。注意看惰性加载的语法:`loadChildren` 后面紧跟着一个字符串,它指向模块的相对路径,然后是一个 `#`,然后是该模块的类名。 diff --git a/aio/content/guide/npm-packages.md b/aio/content/guide/npm-packages.md index 5191bac32f..e22804b7f9 100644 --- a/aio/content/guide/npm-packages.md +++ b/aio/content/guide/npm-packages.md @@ -16,7 +16,7 @@ Alternatively, you can use the [yarn client](https://yarnpkg.com/) for downloadi
-See [Getting Started](guide/quickstart#prerequisites) for information about the required versions and installation of Node.js and npm. +See [Local Environment Setup](guide/setup-local "Setting up for Local Development") for information about the required versions and installation of `Node.js` and `npm`. 参见[快速起步](guide/quickstart#prerequisites),以了解所需的 Node.js 和 npm 版本。 diff --git a/aio/content/guide/prerequisites-setup.md b/aio/content/guide/prerequisites-setup.md deleted file mode 100644 index 2151f2991f..0000000000 --- a/aio/content/guide/prerequisites-setup.md +++ /dev/null @@ -1,148 +0,0 @@ -# Prerequisites and Setup / Creating a workspace / Local development / Local environment - -This guide describes how to get started with local development. - -It includes: -* Prerequisites -* How to install the Angular CLI -* How to create a workspace and initial app project -* How to serve an app project locally -* Additional resources - -{@a devenv} -{@a prerequisites} -## Prerequisites - - -{@a nodejs} -### Node.js - -Angular requires `Node.js` version 8.x or 10.x. - -* To check your version, run `node -v` in a terminal/console window. - -* To get `Node.js`, go to [nodejs.org](https://nodejs.org "Nodejs.org"). - -{@a npm} -### npm package manager: npm or yarn - -Angular, the Angular CLI, and Angular apps depend on features and functionality provided by libraries that are available as [npm packages](https://docs.npmjs.com/getting-started/what-is-npm). To download and install npm packages, you must have an npm package manager. - -The following package managers have been verified with Angular: - -* The [npm client](https://docs.npmjs.com/cli/npm) command line interface, which is installed with `Node.js` by default. To check if you have the npm client installed, run `npm -v` in a terminal/console window. Most of the documentation for Angular assumes the npm client. - -* The [yarn client](https://yarnpkg.com/) command line interface. - -{@a install-cli} - -## Step 1: Install the Angular CLI - -You use the Angular CLI -to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment. - -Install the Angular CLI globally. - -To install the CLI using `npm`, open a terminal/console window and enter the following command: - - - - npm install -g @angular/cli - - - - - -{@a create-proj} - -## Step 2: Create a workspace and initial application - -You develop apps in the context of an Angular [**workspace**](guide/glossary#workspace). A workspace contains the files for one or more [**projects**](guide/glossary/#project). A project is the set of files that comprise an app, a library, or end-to-end (e2e) tests. - -To create a new workspace and initial app project: - -1. Run the CLI command `ng new` and provide the name `my-app`, as shown here: - - - ng new my-app - - - -2. The `ng new` command prompts you for information about features to include in the initial app project. Accept the defaults by pressing the Enter or Return key. - -The Angular CLI installs the necessary Angular npm packages and other dependencies. This can take a few minutes. - -It also creates the following workspace and starter project files: - -* A new workspace, with a root folder named `my-app` -* An initial skeleton app project, also called `my-app` (in the `src` subfolder) -* An end-to-end test project (in the `e2e` subfolder) -* Related configuration files - -The initial app project contains a simple Welcome app, ready to run. - -{@a serve} - -## Step 3: Serve the application - -Angular includes a server, so that you can easily build and serve your app locally. - -1. Go to the workspace folder (`my-app`). - -1. Launch the server by using the CLI command `ng serve`, with the `--open` option. - - - cd my-app - ng serve --open - - -The `ng serve` command launches the server, watches your files, -and rebuilds the app as you make changes to those files. - -The `--open` (or just `-o`) option automatically opens your browser -to `http://localhost:4200/`. - -Your app greets you with a message: - - -
- Welcome to my-app! -
- - - - -## Additional resources - -If you're new to Angular: - -* The [Getting Started](tutorial/) provides hands-on learning. It walks you through the steps to build your first app in an online environment and then deploy that app to your local system. While building a basic catalog and shopping cart app, you'll be introduced to components (the building blocks of Angular), Angular's HTML template syntax, basic display and navigation between views, using services and external data, and scaling and tuning your app. - -* The [Tour of Heroes tutorial](tutorial "Tour of Heroes tutorial") provides additional hands-on learning. It walks you through the steps to build an app that helps a staffing agency manage a group of superhero employees. All of the steps are done locally. - - -* The [Architecture guide](guide/architecture "Architecture guide") describes key concepts such as modules, components, services, and dependency injection (DI). It provides a foundation for more in-depth guides about specific Angular concepts and features. - -After the Tutorial and Architecture guide, you'll be ready to continue exploring Angular on your own through the other guides and references in this documentation set, focusing on the features most important for your apps. - - - - -## Related technologies and tools - -Angular assumes specific versions of many related technologies and tools, such as TypeScript, Karma, Protractor, tsickle, zone.js. - -The `package.json` is organized into two groups of packages: - -* [Dependencies](guide/npm-packages#dependencies) are essential to *running* applications. -* [DevDependencies](guide/npm-packages#dev-dependencies) are only necessary to *develop* applications. - -These packages are described in more detail in [Workspace dependencies](guide/npm-packages). - - - -{@a others} -## Managing different development environments - -If you already have projects running on your machine that use other versions of Node.js and npm, consider using [nvm](https://github.com/creationix/nvm) on Mac or Linux, or [nvm-windows](https://github.com/coreybutler/nvm-windows) on Windows, to manage the multiple versions of Node.js and npm. - diff --git a/aio/content/guide/quickstart.md b/aio/content/guide/quickstart.md deleted file mode 100644 index 9d8d8b53c7..0000000000 --- a/aio/content/guide/quickstart.md +++ /dev/null @@ -1,304 +0,0 @@ -# QuickStart to Local Environment Setup and Development - -# 快速上手 - -Welcome to Angular! Angular helps you build modern applications for the web, mobile, or desktop. - -欢迎使用 Angular!Angular 可以帮助你为 Web、移动端或桌面构建现代应用程序。 - - -
-
Getting Started - Stackblitz
- -
快速上手 - Stackblitz
- - -We recently introduced a [**new Getting Started**](getting-started) that leverages the [StackBlitz](https://stackblitz.com/) online development environment. -We recommend the new Getting Started for anyone who wants to quickly learn the essentials of Angular, in the context of building a basic online store app. - -我们最近引入了一个[**新的快速上手**](getting-started),它改用 [StackBlitz](https://stackblitz.com/) 作为在线开发环境。 -我们建议想要快速学习 Angular 基础知识的同学使用这个新的快速上手,尝试构建一个最基本的在线商店应用。 - -
- - -This guide shows you how to build and run a simple Angular app in your local development environment using the [Angular CLI tool](cli "CLI command reference"). -At the end of this guide—as part of final code review—there is a link to download a copy of the final application code, so that you can compare your work, validate your local setup, or just explore a simple Angular app. -This guide takes less than 30 minutes to complete. - -本指南会告诉你如何在本地开发环境中使用 [Angular CLI 工具](cli "CLI command reference")来构建并运行一个简单的 Angular 应用。 -本指南末尾的最终代码回顾部分提供了一个链接,它可以下载最终的应用代码,好让你能对比自己的工作成果,验证你的本地开发环境,或者浏览一个简单的 Angular 应用。 -本指南只要不到 30 分钟即可完成。 - -{@a devenv} -{@a prerequisites} -## Prerequisites - -## 先决条件 - -Before you begin, make sure your development environment includes `Node.js®` and an npm package manager. - -在开始之前,请确保你的开发环境已经包含了 `Node.js®` 和 npm 包管理器。 - -{@a nodejs} -### Node.js - -Angular requires `Node.js` version 8.x or 10.x. - -Angular 需要 `Node.js` 的 8.x 或 10.x 版本。 - -* To check your version, run `node -v` in a terminal/console window. - - 要想检查你的版本,请在终端/控制台窗口中运行 `node -v` 命令。 - -* To get `Node.js`, go to [nodejs.org](https://nodejs.org "Nodejs.org"). - - 要想安装 `Node.js`,请访问 [nodejs.org](https://nodejs.org "Nodejs.org")。 - -{@a npm} -### npm package manager - -### npm 包管理器 - -Angular, the Angular CLI, and Angular apps depend on features and functionality provided by libraries that are available as [npm packages](https://docs.npmjs.com/getting-started/what-is-npm). To download and install npm packages, you must have an npm package manager. - -Angular、Angular CLI 和 Angular 应用都依赖于某些库所提供的特性和功能,它们都是 [npm 包](https://docs.npmjs.com/getting-started/what-is-npm)。要下载和安装 npm 包,你必须拥有一个 npm 包管理器。 - -This Quick Start uses the [npm client](https://docs.npmjs.com/cli/install) command line interface, which is installed with `Node.js` by default. - -本 "快速上手" 中使用的是 [npm 客户端](https://docs.npmjs.com/cli/install)命令行界面,在安装 `Node.js` 时就已经默认安装了它。 - -To check that you have the npm client installed, run `npm -v` in a terminal/console window. - -要想检查你是否已经安装了 npm 客户端,请在终端/控制台窗口中运行 `npm -v` 命令。 - -{@a install-cli} - -## Step 1: Install the Angular CLI - -## 第一步:安装 Angular CLI - -You use the Angular CLI -to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment. - -你要使用 Angular CLI 来创建项目、创建应用和库代码,并执行多种开发任务,比如测试、打包和发布。 - -Install the Angular CLI globally. - -全局安装 Angular CLI。 - -To install the CLI using `npm`, open a terminal/console window and enter the following command: - -要想使用 `npm` 来安装 CLI,请打开终端/控制台窗口,并输入下列命令: - - - npm install -g @angular/cli - - - - - -{@a create-proj} - -## Step 2: Create a workspace and initial application - -## 第二步:创建工作区和初始应用 - -You develop apps in the context of an Angular [**workspace**](guide/glossary#workspace). A workspace contains the files for one or more [**projects**](guide/glossary/#project). A project is the set of files that comprise an app, a library, or end-to-end (e2e) tests. - -Angular [**工作区**](guide/glossary#workspace)就是你开发应用的上下文环境。 -每个工作区包含一些供一个或多个[**项目**](guide/glossary/#project)使用的文件。 -每个项目都是一组由应用、库或端到端(e2e)测试构成的文件。 - -To create a new workspace and initial app project: - -要想创建工作区和初始应用项目: - -1. Run the CLI command `ng new` and provide the name `my-app`, as shown here: - - 运行 CLI 命令 `ng new`,并提供一个名字 `my-app`,如下所示: - - - ng new my-app - - - -2. The `ng new` command prompts you for information about features to include in the initial app project. Accept the defaults by pressing the Enter or Return key. - - `ng new` 会提示你要把哪些特性包含在初始的应用项目中。请按回车键接受默认值。 - -The Angular CLI installs the necessary Angular npm packages and other dependencies. This can take a few minutes. - -Angular CLI 会安装必要的 Angular npm 包及其它依赖。这可能要花几分钟。 - -It also creates the following workspace and starter project files: - -还将创建下列工作区和初始项目文件: - -* A new workspace, with a root folder named `my-app` - - 一个新的工作区,根目录名叫 `my-app` - -* An initial skeleton app project, also called `my-app` (in the `src` subfolder) - - 一个初始的骨架应用项目,也叫 `my-app`(但位于 `src` 子目录下) - -* An end-to-end test project (in the `e2e` subfolder) - - 一个端到端测试项目(位于 `e2e` 子目录下) - -* Related configuration files - - 相关的配置文件 - -The initial app project contains a simple Welcome app, ready to run. - -初始的应用项目是一个简单的 "欢迎" 应用,随时可以运行它。 - -{@a serve} - -## Step 3: Serve the application - -## 第三步:启动开发服务器 - -Angular includes a server, so that you can easily build and serve your app locally. - -Angular 包含一个开发服务器,以便你能轻易地在本地构建应用和启动开发服务器。 - -1. Go to the workspace folder (`my-app`). - - 进入工作区目录(`my-app`)。 - -1. Launch the server by using the CLI command `ng serve`, with the `--open` option. - - 使用 CLI 命令 `ng serve` 启动开发服务器,并带上 `--open` 选项。 - - - cd my-app - ng serve --open - - -The `ng serve` command launches the server, watches your files, -and rebuilds the app as you make changes to those files. - -`ng serve` 命令会自动启动服务器,并监视你的文件变化,当你修改这些文件时,它就会重新构建应用。 - -The `--open` (or just `-o`) option automatically opens your browser -to `http://localhost:4200/`. - -`--open`(或只用 `-o`)选项会自动打开浏览器,并访问 `http://localhost:4200/`。 - -Your app greets you with a message: - -看,你的应用正在使用一条消息欢迎你: - -
- Welcome to my-app! -
- - - -{@a first-component} - -## Step 4: Edit your first Angular component - -## 第四步:编辑你的第一个 Angular 组件 - -[**_Components_**](guide/glossary#component) are the fundamental building blocks of Angular applications. -They display data on the screen, listen for user input, and take action based on that input. - -[**组件**](guide/glossary#component) 是 Angular 应用中的基本构造块。 -它们在屏幕上显示数据、监听用户输入,并根据这些输入采取行动。 - -As part of the initial app, the CLI created the first Angular component for you. It is the _root component_, and it is named `app-root`. - -作为初始应用的一部分,CLI 也会为你创建第一个 Angular 组件。它就是*根组件*,名叫 `app-root`。 - -1. Open `./src/app/app.component.ts`. - - 打开 `./src/app/app.component.ts`。 - -2. Change the `title` property from `'my-app'` to `'My First Angular App'`. - - 把 `title` 属性从 `'my-app'` 修改成 `'My First Angular App'`。 - - - - The browser reloads automatically with the revised title. That's nice, but it could look better. - - 浏览器将会用修改过的标题自动刷新。很不错,但还可以更好看。 - -3. Open `./src/app/app.component.css` and give the component some style. - - 打开 `./src/app/app.component.css` 并给这个组件提供一些样式。 - - - -Looking good! - -漂亮多了! - -
- Output of Getting Started app -
- - - - -{@a project-file-review} - -## Final code review - -## 最终代码回顾 - -You can download an example of the app that you created in this Getting Started guide. - -你可以下载在本章中创建的这个例子。 - -
- -**Tip:** Most Angular guides include links to download example files and run live examples in [Stackblitz](http://www.stackblitz.com), so that you can see Angular concepts and code in action. - -**提示:** 这里的大多数 Angular 章节中都包含同时下载范例文件和通过 [Stackblitz](http://www.stackblitz.com) 在线运行它的链接,这样你就能在实战中观察这些 Angular 的概念和代码。 - -
- - -For more information about Angular project files and the file structure, see [Workspace and project file structure](guide/file-structure). - -要了解关于 Angular 项目文件和文件结构的更多信息,请参见[工作区与项目的文件结构](guide/file-structure)。 - - -## Next steps - -## 下一步 - -Now that you've seen the essentials of an Angular app and the Angular CLI, continue with these other introductory materials: - -现在,你已经了解了 Angular 和 Angular CLI 的基础知识,请继续访问下列介绍性材料: - -* The [Tour of Heroes tutorial](tutorial "Tour of Heroes tutorial") provides additional hands-on learning. It walks you through the steps to build an app that helps a staffing agency manage a group of superhero employees. -It has many of the features you'd expect to find in a data-driven application: - - [英雄指南教程](tutorial "Tour of Heroes tutorial")提供了更多的亲手演练。它将引导你完成构建应用程序的那些步骤。该应用可以帮助管理机构管理一些身为超级英雄的员工。 - 它具有你在数据驱动应用中所能看到的许多特性: - - - Acquiring and displaying a list of items - - 获取与显示条目的列表 - - - Editing a selected item's detail - - 编辑所选条目的详情 - - - Navigating among different views of the data - - 在数据的不同视图之间导航 - -* The [Architecture guide](guide/architecture "Architecture guide") describes key concepts such as modules, components, services, and dependency injection (DI). It provides a foundation for more in-depth guides about specific Angular concepts and features. - - [架构](guide/architecture "Architecture guide")描述了一些关键概念,比如模块、组件、服务和依赖注入(DI)。它为你深入了解一些 Angular 专属的概念和特性奠定了基础。 - -After the Tutorial and Architecture guide, you'll be ready to continue exploring Angular on your own through the other guides and references in this documentation set, focusing on the features most important for your apps. - -在读完 "英雄指南" 和 "架构" 之后,就可以通过本文档中的其它指南和参考资料自行探索 Angular 了,你可以重点关注那些对你的应用至关重要的特性。 diff --git a/aio/content/guide/releases.md b/aio/content/guide/releases.md index 65261fd796..8cd5181dbd 100644 --- a/aio/content/guide/releases.md +++ b/aio/content/guide/releases.md @@ -148,9 +148,9 @@ The following table contains our current target release dates for the next two m Date | Stable Release | Compatibility ---------------------- | -------------- | ------------- 日期 | 稳定版 | 兼容性 - 2019 年 5 月 | 8.0.0 | ^7.0.0 2019 年 10/11 月 | 9.0.0 | ^8.0.0 - + 2020 年 5 月 | 10.0.0 | ^9.0.0 + Compatibility note: The primary goal of the backward compatibility promise is to ensure that changes in the core framework and tooling don't break the existing ecosystem of components and applications and don't put undue upgrade/migration burden on Angular application and component authors. 兼容性说明:向后兼容性承诺的主要目标是确保在核心框架和核心工具中的变化不会破坏现有组件和应用的生态系统,并且不要给 Angular 应用和组件的开发者带来额外的升级/迁移负担。 @@ -182,7 +182,7 @@ The following table provides the status for Angular versions under support. Version | Status | Released | Active Ends | LTS Ends ------- | ------ | ------------ | ------------ | ------------ 版本 | 状态 | 发布 | 停止活动 | LTS 结束 -^8.0.0 | Active | May 22, 2019 | Nov 22, 2019 | Nov 22, 2020 +^8.0.0 | Active | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020 ^7.0.0 | LTS | Oct 18, 2018 | Apr 18, 2019 | Apr 18, 2020 ^7.0.0 | 活动 | 2018年10月18日 | 2019年4月18日 | 2020年4月18日 ^6.0.0 | LTS | May 3, 2018 | Nov 3, 2018 | Nov 3, 2019 diff --git a/aio/content/guide/router.md b/aio/content/guide/router.md index 1c5615fc43..964cdd6458 100644 --- a/aio/content/guide/router.md +++ b/aio/content/guide/router.md @@ -5372,9 +5372,10 @@ Open the `AppRoutingModule` and add a new `admin` route to its `appRoutes` array 打开 `AppRoutingModule`,并把一个新的 `admin` 路由添加到它的 `appRoutes` 数组中。 -Give it a `loadChildren` property instead of a `children` property, set to the address of the `AdminModule`. -The address is the `AdminModule` file location (relative to the app root), -followed by a `#` separator, followed by the name of the exported module class, `AdminModule`. +Give it a `loadChildren` property instead of a `children` property. +The `loadChildren` property takes a function that returns a promise using the browser's built-in syntax for lazy loading code using dynamic imports `import('...')`. +The path is the location of the `AdminModule` (relative to the app root). +After the code is requested and loaded, the `Promise` resolves an object that contains the `NgModule`, in this case the `AdminModule`. 给它一个 `loadChildren` 属性(替换掉 `children` 属性),把它设置为 `AdminModule` 的地址。 该地址是 `AdminModule` 的文件路径(相对于 `app` 目录的),加上一个 `#` 分隔符,再加上导出模块的类名 `AdminModule`。 diff --git a/aio/content/guide/service-worker-getting-started.md b/aio/content/guide/service-worker-getting-started.md index 5689b8bbbf..b617b10b68 100644 --- a/aio/content/guide/service-worker-getting-started.md +++ b/aio/content/guide/service-worker-getting-started.md @@ -117,6 +117,15 @@ With the server running, you can point your browser at http://localhost:8080/. Y **提示:** 当测试 Angular Service Worker 时,最好使用浏览器中的隐身或隐私窗口,以确保 Service Worker 不会从以前的残留状态中读取数据,否则可能导致意外的行为。 +
+ +**Note:** +If you are not using HTTPS, the service worker will only be registered when accessing the app on `localhost`. + +**注意:** +如果没有使用 HTTPS,那么 Service Worker 只会在 `localhost` 上的应用中进行注册。 +
+ ### Simulating a network issue ### 模拟网络出问题 @@ -300,3 +309,4 @@ You may also be interested in the following: * [Communicating with service workers](guide/service-worker-communications). [与 Service Worker 通讯](guide/service-worker-communications)。 + diff --git a/aio/content/guide/service-worker-intro.md b/aio/content/guide/service-worker-intro.md index 2ca1d1b183..1c127fc31b 100644 --- a/aio/content/guide/service-worker-intro.md +++ b/aio/content/guide/service-worker-intro.md @@ -17,13 +17,13 @@ Service Worker 的功能就像一个网络代理。它们会拦截所有由应 这种代理行为不会局限于通过程序调用 API(比如`fetch`)发起的请求,还包括 HTML 中对资源的引用,甚至对 `index.html` 的首次请求。 基于 Service Worker 的缓存是完全可编程的,并且不依赖于服务端指定的那些控制缓存策略的头。 -Unlike the other scripts that make up an application, such as the Angular app bundle, the service worker is preserved after the user closes the tab. The next time that browser loads the application, the service worker loads first, and can intercept every request for resources to load the application. If the service worker is designed to do so, it can *completely satisfy the loading of the application, without the need for the network*. +Unlike the other scripts that make up an application, such as the Angular app bundle, the service worker is preserved after the user closes the tab. The next time that browser loads the application, the service worker loads first, and can intercept every request for resources to load the application. If the service worker is designed to do so, it can *completely satisfy the loading of the application, without the need for the network*. 不像应用中的其它脚本(如 Angular 的应用包),Service Worker 在用户关闭浏览器页标签时仍然会被保留。 下次浏览器加载本应用时,Service Worker 会首先加载,然后拦截加载本应用时的对每一项资源的请求。 如果这个 Service Worker 就是为此而设计的,它就能*完全满足应用加载时的需求,而不需要依赖网络*。 -Even across a fast reliable network, round-trip delays can introduce significant latency when loading the application. Using a service worker to reduce dependency on the network can significantly improve the user experience. +Even across a fast reliable network, round-trip delays can introduce significant latency when loading the application. Using a service worker to reduce dependency on the network can significantly improve the user experience. 即使在快速可靠的网络中,往返延迟也可能在加载应用程序时产生显著的延迟。使用 Service Worker 来减少对网络的依赖可以显着改善用户体验。 @@ -37,7 +37,7 @@ Angular applications, as single-page applications, are in a prime position to be 从 Angular v5.0.0 开始,Angular 提供了一份 Service Worker 的实现。 Angular 开发人员可以利用 Service Worker,并受益于其增强的可靠性和性能,而无需再针对底层 API 写代码。 -Angular's service worker is designed to optimize the end user experience of using an application over a slow or unreliable network connection, while also minimizing the risks of serving outdated content. +Angular's service worker is designed to optimize the end user experience of using an application over a slow or unreliable network connection, while also minimizing the risks of serving outdated content. Angular 的 Service Worker 的设计目标是优化那些使用慢速、不可靠网络的最终用户的体验,同时还要尽可能减小提供过期内容的风险。 @@ -88,20 +88,28 @@ Your application must run in a web browser that supports service workers. Curren 你的应用必须运行在支持 Service Worker 的 Web 浏览器中。目前,Chrome 和 Firefox 的最新版本 都已经支持了。 要想知道其它浏览器是否支持,参见 [Can I Use](http://caniuse.com/#feat=serviceworkers) 页。 +In addition, in order for service workers to be registered, the app must be accessed over HTTPS, not HTTP. Browsers will ignore service workers on pages that are served over an insecure connection. The reason is that service workers are quite powerful, so extra care needs to be taken to ensure the service worker script has not been tampered with. + +此外,为了注册 Service Worker,应用必须通过 HTTPS 进行访问,而不能通过 HTTP。浏览器会忽略那些通过不安全连接提供的页面上的 Service Worker。其原因在于 Service Worker 真的很强大,所以需要额外的安全保障来确保 Service Worker 脚本不会被中间人攻击所篡改。 + +There is one exception to this rule: To make local development easier, browsers do _not_ require a secure connection when accessing an app on `localhost`. + +这条规则有一个例外:为了方便本地开发,当访问 `localhost` 上的应用时,浏览器*不*要求安全连接。 + ## Related resources ## 相关资源 -For more information about service workers in general, see [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/). +For more information about service workers in general, see [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/). 要了解更多关于 Service Worker 的普遍性信息,参见 [Service Worker 简介](https://developers.google.com/web/fundamentals/primers/service-workers/)。 -For more information about browser support, see the [browser support](https://developers.google.com/web/fundamentals/primers/service-workers/#browser_support) section of [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/), Jake Archibald's [Is Serviceworker ready?](https://jakearchibald.github.io/isserviceworkerready/), and -[Can I Use](http://caniuse.com/#feat=serviceworkers). +For more information about browser support, see the [browser support](https://developers.google.com/web/fundamentals/primers/service-workers/#browser_support) section of [Service Workers: an Introduction](https://developers.google.com/web/fundamentals/primers/service-workers/), Jake Archibald's [Is Serviceworker ready?](https://jakearchibald.github.io/isserviceworkerready/), and +[Can I Use](http://caniuse.com/#feat=serviceworkers). 要了解关于浏览器支持度的更多信息,参见 [Service Worker 简介](https://developers.google.com/web/fundamentals/primers/service-workers/) 中的[浏览器支持](https://developers.google.com/web/fundamentals/primers/service-workers/#browser_support)部分、Jake Archibald 写的[Serviceworker 好了吗?](https://jakearchibald.github.io/isserviceworkerready/)和 [Can I Use](http://caniuse.com/#feat=serviceworkers)。 -The remainder of this Angular documentation specifically addresses the Angular implementation of service workers. +The remainder of this Angular documentation specifically addresses the Angular implementation of service workers. 这份 Angular 文档的其它部分全都专注于讲 Angular 中的 Service Worker 实现。 diff --git a/aio/content/guide/setup-local.md b/aio/content/guide/setup-local.md new file mode 100644 index 0000000000..bb494c6d77 --- /dev/null +++ b/aio/content/guide/setup-local.md @@ -0,0 +1,129 @@ +# Setting up the Local Environment and Workspace + + +This guide explains how to set up your environment for Angular development using the [Angular CLI tool](cli "CLI command reference"). +It includes information about prerequisites, installing the CLI, creating an initial workspace and starter app, and running that app locally to verify your setup. + + +
+
Learning Angular
+ +If you are new to Angular, see [Getting Started](start). Getting Started helps you quickly learn the essentials of Angular, in the context of building a basic online store app. It leverages the [StackBlitz](https://stackblitz.com/) online development environment, so you don't need to set up your local environment until you're ready. + + +
+ + +{@a devenv} +{@a prerequisites} +## Prerequisites + +Before you begin, make sure your development environment includes `Node.js®` and an npm package manager. + +{@a nodejs} +### Node.js + +Angular requires `Node.js` version 10.9.0 or later. + +* To check your version, run `node -v` in a terminal/console window. + +* To get `Node.js`, go to [nodejs.org](https://nodejs.org "Nodejs.org"). + +{@a npm} +### npm package manager + +Angular, the Angular CLI, and Angular apps depend on features and functionality provided by libraries that are available as [npm packages](https://docs.npmjs.com/getting-started/what-is-npm). To download and install npm packages, you must have an npm package manager. + +This setup guide uses the [npm client](https://docs.npmjs.com/cli/install) command line interface, which is installed with `Node.js` by default. + +To check that you have the npm client installed, run `npm -v` in a terminal/console window. + + +{@a install-cli} + +## Step 1: Install the Angular CLI + +You use the Angular CLI +to create projects, generate application and library code, and perform a variety of ongoing development tasks such as testing, bundling, and deployment. + +Install the Angular CLI globally. + +To install the CLI using `npm`, open a terminal/console window and enter the following command: + + + + npm install -g @angular/cli + + + + + +{@a create-proj} + +## Step 2: Create a workspace and initial application + +You develop apps in the context of an Angular [**workspace**](guide/glossary#workspace). + +To create a new workspace and initial starter app: + +1. Run the CLI command `ng new` and provide the name `my-app`, as shown here: + + + ng new my-app + + + +2. The `ng new` command prompts you for information about features to include in the initial app. Accept the defaults by pressing the Enter or Return key. + +The Angular CLI installs the necessary Angular npm packages and other dependencies. This can take a few minutes. + +The CLI creates a new workspace and a simple Welcome app, ready to run. + + +{@a serve} + +## Step 3: Run the application + +The Angular CLI includes a server, so that you can easily build and serve your app locally. + +1. Go to the workspace folder (`my-app`). + +1. Launch the server by using the CLI command `ng serve`, with the `--open` option. + + + cd my-app + ng serve --open + + +The `ng serve` command launches the server, watches your files, +and rebuilds the app as you make changes to those files. + +The `--open` (or just `-o`) option automatically opens your browser +to `http://localhost:4200/`. + +Your app greets you with a message: + + +
+ Welcome to my-app! +
+ + +## Next steps + + +* If you are new to Angular, see the [Getting Started](start) tutorial. Getting Started helps you quickly learn the essentials of Angular, in the context of building a basic online store app. + +
+ + Getting Started assumes the [StackBlitz](https://stackblitz.com/) online development environment. + To learn how to export an app from StackBlitz to your local environment, skip ahead to the [Deployment](start/deployment "Getting Started: Deployment") section. + +
+ + +* To learn more about using the Angular CLI, see the [CLI Overview](cli "CLI Overview"). In addition to creating the initial workspace and app scaffolding, you can use the CLI to generate Angular code such as components and services. The CLI supports the full development cycle, including building, testing, bundling, and deployment. + + +* For more information about the Angular files generated by `ng new`, see [Workspace and Project File Structure](guide/file-structure). + diff --git a/aio/content/guide/setup.md b/aio/content/guide/setup.md index 1effb8ea14..86560383ad 100644 --- a/aio/content/guide/setup.md +++ b/aio/content/guide/setup.md @@ -1,28 +1,38 @@ -# Setup for local development +# Setup for Upgrading from AngularJS # 搭建本地开发环境 -{@a develop-locally} + +
+ +**Audience:** Use this guide **only** in the context of [Upgrading from AngularJS](guide/upgrade "Upgrading from AngularJS to Angular") or [Upgrading for Performance](guide/upgrade-performance "Upgrading for Performance"). +Those Upgrade guides refer to this Setup guide for information about using the [deprecated QuickStart GitHub repository](https://github.com/angular/quickstart "Deprecated Angular QuickStart GitHub repository"), which was created prior to the current Angular [CLI](cli "CLI Overview"). + +**For all other scenarios,** see the current instructions in [Local Environment Setup](guide/setup-local "Setting up for Local Development"). + + +
+ + -《快速上手》在线编程例子是 Angular 的*游乐场*。 - 它不是开发真实应用的地方。 - 你应该在自己的电脑上[本地开发](guide/setup#why-locally "为什么在本地开发?")... 你也应该在本地环境学习 Angular。 +This guide describes how to develop locally on your own machine. +Setting up a new project on your machine is quick and easy with the [QuickStart seed on github](https://github.com/angular/quickstart "Install the github QuickStart repo"). -Setting up a new project on your machine is quick and easy with the **QuickStart seed**, -maintained [on github](https://github.com/angular/quickstart "Install the github QuickStart repo"). +本指南讲的是如何在你自己的机器上进行本地化开发。 +利用 [github 上的**《快速上手》种子**](https://github.com/angular/quickstart "安装 github 《快速上手》库")在你的电脑上搭建一个新项目是很快很容易的。 -利用 [github 上](https://github.com/angular/quickstart "安装 github 《快速上手》库")的**《快速上手》种子**在你的电脑上搭建一个新项目是很快很容易的。 +**Prerequisite:** Make sure you have [Node.js® and npm installed](guide/setup-local#prerequisites "Angular prerequisites"). -Make sure you have [Node.js® and npm installed](guide/setup#install-prerequisites "What if you don't have Node.js and npm?"). - -请确保你已经安装了 [Node.js® 和 npm](guide/setup#install-prerequisites "如果你没有 Node.js 和 npm?")。 +**先决条件:**确保你已经安装好了 [Node.js® 和 npm](guide/setup-local#prerequisites "Angular prerequisites")。 {@a clone} - ## Clone ## 克隆 @@ -122,9 +132,8 @@ Open a terminal window in the project folder and enter the following commands fo ## 《快速上手》种子库里都有什么? -The **QuickStart seed** contains the same application as the QuickStart playground. -But its true purpose is to provide a solid foundation for _local_ development. -Consequently, there are _many more files_ in the project folder on your machine, +The **QuickStart seed** provides a basic QuickStart playground application and other files necessary for local development. +Consequently, there are many files in the project folder on your machine, most of which you can [learn about later](guide/file-structure). **《快速上手》种子** 包含了与《快速上手》游乐场一样的应用,但是,它真正的目的是提供坚实的*本地*开发基础。 @@ -364,86 +373,26 @@ You may need [nvm](https://github.com/creationix/nvm) if you already have projec 我们推荐使用 [nvm](https://github.com/creationix/nvm) 来管理多版本 Node.js 和 npm。 如果你的电脑上已经有使用其他版本 Node.js 和 npm 的项目,你可能需要 nvm。 -{@a why-locally} -## Appendix: Why develop locally +## Appendix: Develop locally with IE -## 附录:为何在本地开发 +## 附录:用 IE 进行本地化开发 -Live coding in the browser is a great way to explore Angular. +If you develop angular locally with `ng serve`, a `websocket` connection is set up automatically between browser and local dev server, so when your code changes, the browser can automatically refresh. -在浏览器中在线编程是很好的探索 Angular 的方法。 +如果你使用 `ng serve` 进行本地化 Angular 开发,就会自动在浏览器和本地开发服务器之间建立一个 `websocket` 连接,这样,在代码发生变化时,浏览器就会自动刷新。 -Links on almost every documentation page open completed samples in the browser. -You can play with the sample code, share your changes with friends, and download and run the code on your own machine. +In Windows, by default, one application can only have 6 websocket connections, MSDN WebSocket Settings. +So when IE is refreshed (manually or automatically by `ng serve`), sometimes the websocket does not close properly. When websocket connections exceed the limitations, a `SecurityError` will be thrown. This error will not affect the angular application, you can just restart IE to clear this error, or modify the windows registry to update the limitations. -几乎每章文档里面的链接都在浏览器中打开完整的例子。 -你可以用这些代码做实验,或者与朋友共享你的修改,或者下载并在你自己的电脑上运行这些代码。 +在 Windows 上,默认情况下,每个应用最多只能有 6 个 websocket 连接,参见 MSDN 上的 WebSocket 设置。 +所以,当 IE 刷新时(手动刷新或由 `ng serve` 自动刷新),websocket 可能无法正常关闭。当 websocket 连接数超过上限时,就会抛出一个 `SecurityError` 异常。这种错误不会影响 Angular 应用,你可以重启 IE 来清除此异常或在 Windows 注册表中加大这个上限。 -The [Getting Started](guide/quickstart "Angular QuickStart Playground") shows just the `AppComponent` file. -It creates the equivalent of `app.module.ts` and `main.ts` internally _for the playground only_. -so the reader can discover Angular without distraction. -The other samples are based on the QuickStart seed. - -[快速上手](guide/quickstart "Angular 快速起步游乐场")仅仅展示了 `AppComponent` 文件。 -它在内部创建了只为*游乐场*而准备的等价 `app.module.ts` 和 `main.ts`。 -所以读者可以在零干扰的情况下探索 Angular。 -其他例子是基于 《快速上手》种子的。 - -As much fun as this is ... - -虽然有这么多的乐趣,但是... - -* you can't ship your app in Stackblitz - - 你不能在 Stackblitz 里面发布你的应用 - -* you aren't always online when writing code - - 编程时你不可能总是在线 - -* transpiling TypeScript in the browser is slow - - 在浏览器中编译 TypeScript 很慢 - -* the type support, refactoring, and code completion only work in your local IDE - - 只有本地 IDE 有类型支持、代码重构和代码自动完成 - -Use the live coding environment as a _playground_, -a place to try the documentation samples and experiment on your own. -It's the perfect place to reproduce a bug when you want to -file a documentation issue or -file an issue with Angular itself. - -把在线编程环境当做*游乐场*,一个尝试文档例子和自己做实验的地方。 -当你想要提交关于文档的问题或者 -提交关于 Angular 自身的问题时, -它是重现错误的完美地方。 - -For real development, we strongly recommend [developing locally](guide/setup#develop-locally). - -对于现实项目开发,我们强烈推荐在[本地开发](guide/setup#develop-locally)。 - -## Appendix: develop locally with IE - -## 附录:使用 IE 进行本地开发 - -If you develop angular locally with `ng serve`, there will be `websocket` connection being setup automatically between browser and local dev server, so when your code change, browser can automatically refresh. - -如果你使用 `ng serve` 在本地进行 Angular 开发,就会在浏览器和本地开发服务器之间自动建立一个 `WebSocket` 连接,因此,当你的代码变化时,浏览器也会自动刷新。 - -In windows, by default one application can only have 6 websocket connections, MSDN WebSocket Settings. -So if IE was refreshed manunally or automatically by `ng serve`, sometimes, the websocket will not close properly, when websocket connections exceed limitations, `SecurityError` will be thrown, this error will not affect the angular application, you can just restart IE to clear this error, or modify the windows registry to update the limitations. - -在 Windows 中,默认情况下一个应用只能有六个 WebSocket 连接,参见 MSDN 中的 WebSocket 设置部分。 -所以,如果 IE 手动刷新或被 `ng serve` 触发了自动刷新,有时候 WebSocket 可能无法正常关闭,当 WebSocket 的连接数超限时,就会抛出 `SecurityError` 异常。请放心,这个异常对 Angular 应用没什么影响,你重启一下 IE 就能消除这个错误,或者修改 Windows 注册表来修改这个上限。 - -## Appendix: test using `fakeAsync()/async()` +## Appendix: Test using `fakeAsync()/async()` ## 附录:使用 `fakeAsync()/async()` 进行测试 -If you use the `fakeAsync()/async()` helper function to run unit tests (for details, read [testing guide](guide/testing#async-test-with-fakeasync)), you need to import `zone.js/dist/zone-testing` in your test setup file. +If you use the `fakeAsync()/async()` helper function to run unit tests (for details, read the [Testing guide](guide/testing#async-test-with-fakeasync)), you need to import `zone.js/dist/zone-testing` in your test setup file. 如果你使用 `fakeAsync()/async()` 辅助函数来运行单元测试(详情参见[测试指南](guide/testing#async-test-with-fakeasync)),就要在测试的准备文件中导入 `zone.js/dist/zone-testing`。 @@ -470,4 +419,4 @@ import 'zone.js/dist/fake-async-test'; You can still load those files separately, but the order is important, you must import `proxy` before `sync-test`, `async-test`, `fake-async-test` and `jasmine-patch`. And you also need to import `sync-test` before `jasmine-patch`, so it is recommended to just import `zone-testing` instead of loading those separated files. -你仍然可以分别导入这些文件,不过导入顺序很重要,你必须在 `sync-test`、`async-test`、`fake-async-test` 和 `jasmine-patch` 之前导入 `proxy`。还要注意在 `jasmine-patch` 之前导入`sync-test`。所以,建议你只导入 `zone-testing` 而不要分别加载那些文件。 \ No newline at end of file +你仍然可以分别导入这些文件,不过导入顺序很重要,你必须在 `sync-test`、`async-test`、`fake-async-test` 和 `jasmine-patch` 之前导入 `proxy`。还要注意在 `jasmine-patch` 之前导入`sync-test`。所以,建议你只导入 `zone-testing` 而不要分别加载那些文件。 diff --git a/aio/content/guide/static-query-migration.md b/aio/content/guide/static-query-migration.md new file mode 100644 index 0000000000..a8e4bd0279 --- /dev/null +++ b/aio/content/guide/static-query-migration.md @@ -0,0 +1,158 @@ +# Static Query Migration Guide +​ +**Important note for library authors: This migration is especially crucial for library authors to facilitate their users upgrading to version 9 when it becomes available (approx Oct 2019).** + +In version 9, the default setting for `@ViewChild` and `@ContentChild` queries is changing in order to fix buggy and surprising behavior in queries (read more about that [here](#what-does-this-flag-mean)). + +In preparation for this change, in version 8, we are migrating all applications and libraries to explicitly specify the resolution strategy for `@ViewChild` and `@ContentChild` queries. + +Specifically, this migration adds an explicit "static" flag that dictates when that query's results should be assigned. +Adding this flag will ensure your code works the same way when upgrading to version 9. + +Before: + +``` +// query results sometimes available in `ngOnInit`, sometimes in `ngAfterViewInit` (based on template) +@ViewChild('foo') foo: ElementRef; +``` + +After: + +``` +// query results available in ngOnInit +@ViewChild('foo', {static: true}) foo: ElementRef; + +OR + +// query results available in ngAfterViewInit +@ViewChild('foo', {static: false}) foo: ElementRef; +``` + +Starting with version 9, the `static` flag will default to false. +At that time, any `{static: false}` flags can be safely removed, and we will have a schematic that will update your code for you. + +Note: this flag only applies to `@ViewChild` and `@ContentChild` queries specifically, as `@ViewChildren` and `@ContentChildren` queries do not have a concept of static and dynamic (they are always resolved as if they are "dynamic"). + +## FAQ + +{@a what-to-do-with-todo} +### What should I do if I see a `/* TODO: add static flag */` comment printed by the schematic? + +If you see this comment, it means that the schematic couldn't statically figure out the correct flag. In this case, you'll have to add the correct flag based on your application's behavior. +For more information on how to choose, see the [next question](#how-do-i-choose). + +{@a how-do-i-choose} +### How do I choose which `static` flag value to use: `true` or `false`? + +In the official API docs, we have always recommended retrieving query results in [`ngAfterViewInit` for view queries](https://angular.io/api/core/ViewChild#description) and [`ngAfterContentInit` for content queries](https://angular.io/api/core/ContentChild#description). +This is because by the time those lifecycle hooks run, change detection has completed for the relevant nodes and we can guarantee that we have collected all the possible query results. + +Most applications will want to use `{static: false}` for the same reason. This setting will ensure query matches that are dependent on binding resolution (e.g. results inside `*ngIf`s or `*ngFor`s) will be found by the query. + +There are rarer cases where `{static: true}` flag might be necessary (see [answer here](#should-i-use-static-true)). + +{@a should-i-use-static-true} +### Is there a case where I should use `{static: true}`? + +This option was introduced to support creating embedded views on the fly. +If you need access to a `TemplateRef` in a query to create a view dynamically, you won't be able to do so in `ngAfterViewInit`. +Change detection has already run on that view, so creating a new view with the template will cause an `ExpressionHasChangedAfterChecked` error to be thrown. +In this case, you will want to set the `static` flag to `true` and create your view in `ngOnInit`. +In most other cases, the best practice is to use `{static: false}`. + +However, to facilitate the migration to version 8, you may also want to set the `static` flag to `true` if your component code already depends on the query results being available some time **before** `ngAfterViewInit` (for view queries) or `ngAfterContentInit` (for content queries). +For example, if your component relies on the query results being populated in the `ngOnInit` hook or in `@Input` setters, you will need to either set the flag to `true` or re-work your component to adjust to later timing. + +Note: Selecting the static option means that query results nested in `*ngIf` or `*ngFor` will not be found by the query. +These results are only retrievable after change detection runs. + +{@a what-does-this-flag-mean} +### What does this flag mean and why is it necessary? + +The default behavior for queries has historically been undocumented and confusing, and has also commonly led to issues that are difficult to debug. +In version 9, we would like to make query behavior more consistent and simple to understand. + +To explain why, first it's important to understand how queries have worked up until now. + +Without the `static` flag, the compiler decided when each query would be resolved on a case-by-case basis. +All `@ViewChild`/`@ContentChild` queries were categorized into one of two buckets at compile time: "static" or "dynamic". +This classification determined when query results would become available to users. + +- **Static queries** were queries where the result could be determined statically because the result didn't depend on runtime values like bindings. +Results from queries classified as static were available before change detection ran for that view (accessible in `ngOnInit`). + +- **Dynamic queries** were queries where the result could NOT be determined statically because the result depended on runtime values (aka bindings). +Results from queries classified as dynamic were not available until after change detection ran for that view (accessible in `ngAfterContentInit` for content queries or `ngAfterViewInit` for view queries). + +For example, let's say we have a component, `Comp`. Inside it, we have this query: + +``` +@ViewChild(Foo) foo: Foo; +``` + +and this template: + +``` +
+``` + +This `Foo` query would be categorized as static because at compile-time it's known that the `Foo` instance on the `
` is the correct result for the query. +Because the query result is not dependent on runtime values, we don't have to wait for change detection to run on the template before resolving the query. +Consequently, results can be made available in `ngOnInit`. + +Let's say the query is the same, but the component template looks like this: + +``` +
+``` + +With that template, the query would be categorized as a dynamic query. +We would need to know the runtime value of `showing` before determining what the correct results are for the query. +As a result, change detection must run first, and results can only be made available in `ngAfterViewInit` or a setter for the query property. + +The effect of this implementation is that adding an `*ngIf` or `*ngFor` anywhere above a query match can change when that query's results become available. + +Keep in mind that these categories only applied to `@ViewChild` and `@ContentChild` queries specifically. +`@ViewChildren` and `@ContentChildren` queries did not have a concept of static and dynamic, so they were always resolved as if they were "dynamic". + +This strategy of resolving queries at different times based on the location of potential query matches has caused a lot of confusion. Namely: + +* Sometimes query results are available in `ngOnInit`, but sometimes they aren't and it's not clear why (see [21800](https://github.com/angular/angular/issues/21800) or [19872](https://github.com/angular/angular/issues/19872)). + +* `@ViewChild` queries are resolved at a different time from `@ViewChildren` queries, and `@ContentChild` queries are resolved at a different time from `@ContentChildren` queries. +If a user turns a `@ViewChild` query into a `@ViewChildren` query, their code can break suddenly because the timing has shifted. + +* Code depending on a query result can suddenly stop working as soon as an `*ngIf` or an `*ngFor` is added to a template. + +* A `@ContentChild` query for the same component will resolve at different times in the lifecycle for each usage of the component. +This leads to buggy behavior where using a component with `*ngIf` is broken in subtle ways that aren't obvious to the component author. + +In version 9, we plan to simplify the behavior so all queries resolve after change detection runs by default. +The location of query matches in the template cannot affect when the query result will become available and suddenly break your code, and the default behavior is always the same. +This makes the logic more consistent and predictable for users. + +That said, if an application does need query results earlier (for example, the query result is needed to create an embedded view), it's possible to add the `{static: true}` flag to explicitly ask for static resolution. +With this flag, users can indicate that they only care about results that are statically available and the query results will be populated before `ngOnInit`. + +{@a view-children-and-content-children} +### Does this change affect `@ViewChildren` or `@ContentChildren` queries? + +No, this change only affects `@ViewChild` and `@ContentChild` queries specifically. +`@ViewChildren` and `@ContentChildren` queries are already "dynamic" by default and don't support static resolution. + +{@a why-specify-static-false} +### ​Why do I have to specify `{static: false}`? Isn't that the default? + +The goal of this migration is to transition apps that aren't yet on version 9 to a query pattern that is compatible with version 9. +However, most applications use libraries, and it's likely that some of these libraries may not be upgraded to version 8 yet (and thus might not have the proper flags). +Since the application's version of Angular will be used for compilation, if we change the default, the behavior of queries in the library's components will change to the version 8 default and possibly break. +This way, an application's dependencies will behave the same way during the transition as they did in the previous version. + +In Angular version 9 and later, it will be safe to remove any `{static: false}` flags and we will do this cleanup for you in a schematic. + +{@a libraries} +### Can I keep on using Angular libraries that haven’t yet updated to version 8 yet? + +Yes, absolutely! +Because we have not changed the default query behavior in version 8 (i.e. the compiler still chooses a timing if no flag is set), when your application runs with a library that has not updated to version 8, the library will run the same way it did in version 7. +This guarantees your app will work in version 8 even if libraries take longer to update their code. diff --git a/aio/content/guide/styleguide.md b/aio/content/guide/styleguide.md index b6fbeeeb54..ca77c0dd27 100644 --- a/aio/content/guide/styleguide.md +++ b/aio/content/guide/styleguide.md @@ -1639,364 +1639,6 @@ A consistent class and file name convention make these modules easy to spot and -Back to top - -回到顶部 - -## Coding conventions - -## 编程约定 - -Have a consistent set of coding, naming, and whitespace conventions. - -坚持一致的编程、命名和空格的约定。 - -{@a 03-01} - -### Classes - -### 类 - -#### Style 03-01 - -#### 风格 03-01 - -
- -**Do** use upper camel case, also known as PascalCase, when naming classes. - -**坚持**使用大写驼峰形式(也叫 Pascal 形式)来命名类。 - -
- -
- -**Why?** Follows conventional thinking for class names. - -**为何?**遵循类命名传统约定。 - -
- -
- -**Why?** Classes can be instantiated and construct an instance. -By convention, upper camel case indicates a constructable asset. - -**为何?**类可以被实例化和构造实例。根据约定,用大写驼峰命名法来标识可构造的东西。 - -
- - - - - - - - - - - -Back to top - -回到顶部 - -{@a 03-02} - -### Constants - -### 常量 - -#### Style 03-02 - -#### 风格 03-02 - -
- -**Do** declare variables with `const` if their values should not change during the application lifetime. - -**坚持**用 `const` 声明变量,除非它们的值在应用的生命周期内会发生变化。 - -
- -
- -**Why?** Conveys to readers that the value is invariant. - -**为何?**告诉读者这个值是不可变的。 - -
- -
- -**Why?** TypeScript helps enforce that intent by requiring immediate initialization and by -preventing subsequent re-assignment. - -**为何?** TypeScript 会要求在声明时立即初始化,并阻止再次赋值,以帮助确保你的设计意图。 - -
- -
- -**Consider** spelling `const` variables in lower camel case. - -**考虑** 把常量名拼写为小驼峰格式。 - -
- -
- -**Why?** Lower camel case variable names (`heroRoutes`) are easier to read and understand -than the traditional UPPER_SNAKE_CASE names (`HERO_ROUTES`). - -**为何?**小驼峰变量名 (`heroRoutes`) 比传统的大写蛇形命名法 (`HERO_ROUTES`) 更容易阅读和理解。 - -
- -
- -**Why?** The tradition of naming constants in UPPER_SNAKE_CASE reflects -an era before the modern IDEs that quickly reveal the `const` declaration. -TypeScript prevents accidental reassignment. - -**为何?** 把常量命名为大写蛇形命名法的传统源于现代 IDE 出现之前, -以便阅读时可以快速发现那些 `const` 定义。 -TypeScript 本身就能够防止意外赋值。 - -
- -
- -**Do** tolerate _existing_ `const` variables that are spelled in UPPER_SNAKE_CASE. - -**坚持**容许*现存的*`const` 常量沿用大写蛇形命名法。 - -
- -
- -**Why?** The tradition of UPPER_SNAKE_CASE remains popular and pervasive, -especially in third party modules. -It is rarely worth the effort to change them at the risk of breaking existing code and documentation. - -**为何?**传统的大写蛇形命名法仍然很流行、很普遍,特别是在第三方模块中。 -修改它们没多大价值,还会有破坏现有代码和文档的风险。 - -
- - - - - -Back to top - -回到顶部 - -{@a 03-03} - -### Interfaces - -### 接口 - -#### Style 03-03 - -#### 风格 03-03 - -
- -**Do** name an interface using upper camel case. - -**坚持**使用大写驼峰命名法来命名接口。 - -
- -
- -**Consider** naming an interface without an `I` prefix. - -**考虑**不要在接口名字前面加 `I` 前缀。 - -
- -
- -**Consider** using a class instead of an interface for services and declarables (components, directives, and pipes). - -**考虑**在服务和可声明对象(组件、指令和管道)中用类代替接口。 - -
- -
- -**Consider** using an interface for data models. - -**考虑**用接口作为数据模型。 - -
- -
- -**Why?** TypeScript guidelines -discourage the `I` prefix. - -**为何?**TypeScript 指导原则不建议使用 “I” 前缀。 - -
- -
- -**Why?** A class alone is less code than a _class-plus-interface_. - -**为何?**单独一个类的代码量小于*类+接口*。 - -
- -
- -**Why?** A class can act as an interface (use `implements` instead of `extends`). - -**为何?**类可以作为接口使用(只是用 `implements` 代替 `extends` 而已)。 - -
- -
- -**Why?** An interface-class can be a provider lookup token in Angular dependency injection. - -**为何?**在 Angular 依赖注入系统中,接口类(译注:指写成类的形式,但是只当做接口使用)可以作为服务提供商的查找令牌。 - -
- - - - - - - - - -Back to top - -回到顶部 - -{@a 03-04} - -### Properties and methods - -### 属性和方法 - -#### Style 03-04 - -#### 样式 03-04 - -
- -**Do** use lower camel case to name properties and methods. - -**坚持**使用小写驼峰命名法来命名属性和方法。 - -
- -
- -**Avoid** prefixing private properties and methods with an underscore. - -**避免**为私有属性和方法添加下划线前缀。 - -
- -
- -**Why?** Follows conventional thinking for properties and methods. - -**为何?**遵循传统属性和方法的命名约定。 - -
- -
- -**Why?** JavaScript lacks a true private property or method. - -**为何?** JavaScript 不支持真正的私有属性和方法。 - -
- -
- -**Why?** TypeScript tooling makes it easy to identify private vs. public properties and methods. - -**为何?** TypeScript 工具让识别私有或公有属性和方法变得很简单。 - -
- - - - - - - - - -Back to top - -回到顶部 - -{@a 03-06} - -### Import line spacing - -### 导入语句中的空行 - -#### Style 03-06 - -#### 风格 03-06 - -
- -**Consider** leaving one empty line between third party imports and application imports. - -**坚持**在第三方导入和应用导入之间留一个空行。 - -
- -
- -**Consider** listing import lines alphabetized by the module. - -**考虑**按模块名字的字母顺排列导入行。 - -
- -
- -**Consider** listing destructured imported symbols alphabetically. - -**考虑**在解构表达式中按字母顺序排列导入的东西。 - -
- -
- -**Why?** The empty line separates _your_ stuff from _their_ stuff. - -**为何?**空行可以让阅读和定位本地导入更加容易。 - -
- -
- -**Why?** Alphabetizing makes it easier to read and locate symbols. - -**为何?**按字母顺序排列可以让阅读和定位本地导入更加容易。 - -
- - - - - - - - Back to top @@ -2372,12 +2014,6 @@ Here is a compliant folder and file structure:
-
- - core.module.ts - -
-
exception.service.ts|spec.ts @@ -2502,14 +2138,12 @@ Here is a compliant folder and file structure:
- text-filter.component.ts|spec.ts - + filter-text.component.ts|spec.ts
- text-filter.service.ts|spec.ts - + filter-text.service.ts|spec.ts
@@ -3058,14 +2692,12 @@ Yet there is a real danger of that happening if the `SharedModule` provides a se
- text-filter.component.ts|spec.ts - + filter-text.component.ts|spec.ts
- text-filter.service.ts|spec.ts - + filter-text.service.ts|spec.ts
diff --git a/aio/content/guide/testing.md b/aio/content/guide/testing.md index e6ed1865ca..4cd42f7333 100644 --- a/aio/content/guide/testing.md +++ b/aio/content/guide/testing.md @@ -339,8 +339,8 @@ Now you can run the following commands to use the `--no-sandbox` flag: 现在你可以运行下列带有 `--no-sandbox` 标志的命令了: - ng test -- --no-watch --no-progress --browsers=ChromeHeadlessCI - ng e2e -- --protractor-config=e2e/protractor-ci.conf.js + ng test --no-watch --no-progress --browsers=ChromeHeadlessCI + ng e2e --protractor-config=e2e/protractor-ci.conf.js
diff --git a/aio/content/guide/typescript-configuration.md b/aio/content/guide/typescript-configuration.md index e8064bc2b9..af5440fe7c 100644 --- a/aio/content/guide/typescript-configuration.md +++ b/aio/content/guide/typescript-configuration.md @@ -155,7 +155,7 @@ The `node_modules/@angular/core/` folder of any Angular application contains sev 很多库在自己的 npm 包中都包含了它们的类型定义文件,TypeScript 编译器和编辑器都能找到它们。Angular 库也是这样的。 任何 Angular 应用程序的 `node_modules/@angular/core/` 目录下,都包含几个 `d.ts` 文件,它们描述了 Angular 的各个部分。 -**You need do nothing to get *typings* files for library packages that include `d.ts` files. +**You don't need to do anything to get *typings* files for library packages that include `d.ts` files. Angular packages include them already.** **你不需要为那些包含了 `d.ts` 文件的库获取*类型定义*文件 —— Angular 的所有包都是如此。** diff --git a/aio/content/guide/upgrade.md b/aio/content/guide/upgrade.md index 1733f33b9f..a75fadca07 100644 --- a/aio/content/guide/upgrade.md +++ b/aio/content/guide/upgrade.md @@ -1302,6 +1302,125 @@ After this, the service is injectable anywhere in AngularJS code: +## Lazy Loading AngularJS + +When building applications, you want to ensure that only the required resources are loaded when necessary. Whether that be loading of assets or code, making sure everything that can be deferred until needed keeps your application running efficiently. This is especially true when running different frameworks in the same application. + +[Lazy loading](guide/glossary#lazy-loading) is a technique that defers the loading of required assets and code resources until they are actually used. This reduces startup time and increases efficiency, especially when running different frameworks in the same application. + +When migrating large applications from AngularJS to Angular using a hybrid approach, you want to migrate some of the most commonly used features first, and only use the less commonly used features if needed. Doing so helps you ensure that the application is still providing a seamless experience for your users while you are migrating. + +In most environments where both Angular and AngularJS are used to render the application, both frameworks are loaded in the initial bundle being sent to the client. This results in both increased bundle size and possible reduced performance. + +Overall application performance is affected in cases where the user stays on Angular-rendered pages because the AngularJS framework and application are still loaded and running, even if they are never accessed. + +You can take steps to mitigate both bundle size and performance issues. By isolating your AngularJS app to a separate bundle, you can take advantage of [lazy loading](guide/glossary#lazy-loading) to load, bootstrap, and render the AngularJS application only when needed. This strategy reduces your initial bundle size, defers any potential impact from loading both frameworks until absolutely necessary, and keeps your application running as efficiently as possible. + +The steps below show you how to do the following: + +* Setup a callback function for your AngularJS bundle. +* Create a service that lazy loads and bootstraps your AngularJS app. +* Create a routable component for AngularJS content +* Create a custom `matcher` function for AngularJS-specific URLs and configure the Angular `Router` with the custom matcher for AngularJS routes. + +### Create a service to lazy load AngularJS + +As of Angular version 8, lazy loading code can be accomplished simply by using the dynamic import syntax `import('...')`. In your application, you create a new service that uses dynamic imports to lazy load AngularJS. + + + + +The service uses the `import()` method to load your bundled AngularJS application lazily. This decreases the initial bundle size of your application as you're not loading code your user doesn't need yet. You also need to provide a way to _bootstrap_ the application manually after it has been loaded. AngularJS provides a way to manually bootstrap an application using the [angular.bootstrap()](https://docs.angularjs.org/api/ng/function/angular.bootstrap) method with a provided HTML element. Your AngularJS app should also expose a `bootstrap` method that bootstraps the AngularJS app. + + + + +Your AngularJS application is configured with only the routes it needs to render content. The remaining routes in your application are handled by the Angular Router. The exposed `bootstrap` method is called in your Angular app to bootstrap the AngularJS application after the bundle is loaded. + +
+ +**Note:** After AngularJS is loaded and bootstrapped, listeners such as those wired up in your route configuration will continue to listen for route changes. To ensure listeners are shut down when AngularJS isn't being displayed, configure an `otherwise` option with the [$routeProvider](https://docs.angularjs.org/api/ngRoute/provider/$routeProvider) that renders an empty template. This assumes all other routes will be handled by Angular. + +
+ +### Create a component to render AngularJS content + +In your Angular application, you need a component as a placeholder for your AngularJS content. This component uses the service you create to load and bootstrap your AngularJS app after the component is initialized. + + + + +When the Angular Router matches a route that uses AngularJS, the `AngularJSComponent` is rendered, and the content is rendered within the AngularJS [`ng-view`](https://docs.angularjs.org/api/ngRoute/directive/ngView) directive. + +### Configure a custom route matcher for AngularJS routes + +To configure the Angular Router, you must define a route for AngularJS URLs. To match those URLs, you add a route configuration that uses the `matcher` property. The `matcher` allows you to use custom pattern matching for URL paths. The Angular Router tries to match on more specific routes such as static and variable routes first. When it doesn't find a match, it then looks at custom matchers defined in your route configuration. If the custom matchers don't match a route, it then goes to catch-all routes, such as a 404 page. + +The following example defines a custom matcher function for AngularJS routes. + + + + +The following code adds a route object to your routing configuration using the `matcher` property and custom matcher, and the `component` property with `AngularJSComponent`. + + + + +When your application matches a route that needs AngularJS, the AngularJS app is loaded and bootstrapped, the AngularJS routes match the necessary URL to render their content, and your application continues to run with both AngularJS and Angular frameworks. + +## Using the Unified Angular Location Service + +In AngularJS, the [$location service](https://docs.angularjs.org/api/ng/service/$location) handles all routing configuration and navigation, encoding and decoding of URLS, redirects, and interactions with browser APIs. Angular uses its own underlying `Location` service for all of these tasks. + +When you migrate from AngularJS to Angular you will want to move as much responsibility as possible to Angular, so that you can take advantage of new APIs. To help with the transition, Angular provides the `LocationUpgradeModule`. This module enables a _unified_ location service that shifts responsibilities from the AngularJS `$location` service to the Angular `Location` service. + +To use the `LocationUpgradeModule`, import the symbol from `@angular/common/upgrade` and add it to your `AppModule` imports using the static `LocationUpgradeModule.config()` method. + +```ts +// Other imports ... +import { LocationUpgradeModule } from '@angular/common/upgrade'; + +@NgModule({ + imports: [ + // Other NgModule imports... + LocationUpgradeModule.config() + ] +}) +export class AppModule {} +``` + +The `LocationUpgradeModule.config()` method accepts a configuration object that allows you to configure options including the `LocationStrategy` with the `useHash` property, and the URL prefix with the `hashPrefix` property. + +The `useHash` property defaults to `false`, and the `hashPrefix` defaults to an empty `string`. Pass the configuration object to override the defaults. + +```ts +LocationUpgradeModule.config({ + useHash: true + hashPrefix: '!' +}) +``` + +
+ +**Note:** See the `LocationUpgradeConfig` for more configuration options available to the `LocationUpgradeModule.config()` method. + +
+ +This registers a drop-in replacement for the `$location` provider in AngularJS. Once registered, all navigation, routing broadcast messages, and any necessary digest cycles in AngularJS triggered during navigation are handled by Angular. This gives you a single way to navigate within both sides of your hybrid application consistently. + +For usage of the `$location` service as a provider in AngularJS, you need to downgrade the `$locationShim` using a factory provider. + +```ts +// Other imports ... +import { $locationShim } from '@angular/common/upgrade'; +import { downgradeInjectable } from '@angular/upgrade/static'; + +angular.module('myHybridApp', [...]) + .factory('$location', downgradeInjectable($locationShim)); +``` + +Once you introduce the Angular Router, using the Angular Router triggers navigations through the unified location service, still providing a single source for navigating with AngularJS and Angular. + ## Using Ahead-of-time compilation with hybrid apps ## 在混合式应用中使用 AOT 编译 @@ -1705,7 +1824,7 @@ JavaScript (based on the `tsconfig.json` configuration file): 最后,你应该把下列 npm 脚本添加到 `package.json` 中,用于把 TypeScript 文件编译成 JavaScript (根据 `tsconfig.json` 的配置): - "script": { + "scripts": { "tsc": "tsc", "tsc:w": "tsc -w", ... diff --git a/aio/content/guide/visual-studio-2015.md b/aio/content/guide/visual-studio-2015.md index 20fc70cafe..3ad833a544 100644 --- a/aio/content/guide/visual-studio-2015.md +++ b/aio/content/guide/visual-studio-2015.md @@ -8,15 +8,14 @@ Some developers prefer Visual Studio as their Integrated Development Environment 有些开发者喜欢用 Visual Studio 作为他们的集成开发环境。 -This cookbook describes the steps required to set up and use the -Angular [Getting Started](guide/quickstart) files in Visual Studio 2015 within an ASP.NET 4.x project. +This cookbook describes the steps required to set up and use Angular app files in Visual Studio 2015 within an ASP.NET 4.x project. 本文介绍了在**Visual Studio 2015 的 ASP.NET 4.x 项目中**,实现 Angular “[快速上手](guide/quickstart)”所需的步骤。
There is no *live example* for this cookbook because it describes Visual Studio, not -the Angular Getting Started application itself. +the Angular application itself. It uses the starter Angular application created by the CLI command [`ng new`](cli/new) as an example. 本文中没有*在线例子*,因为它介绍的是 Visual Studio,而不是《快速上手》应用程序本身。 @@ -52,7 +51,7 @@ Note that the resulting code does not map to the docs. Adjust accordingly. Install **[Node.js® and npm](https://nodejs.org/en/download/)** if they are not already on your machine. -See [Getting Started](guide/quickstart) for supported versions and instructions. +See [Local Environment Setup](guide/setup-local "Setting up for Local Development") for supported versions and instructions. 如果你的电脑里没有 Node.js®和 npm,请安装**[它们](https://nodejs.org/en/download/)**。 参见[快速上手](guide/quickstart)以了解所支持的版本和安装步骤。 @@ -116,24 +115,23 @@ Visual Studio 将优先在当前的工作区查找外部工具,如果没有找

前提条件: 安装 TypeScript 2.2 for Visual Studio 2015

-While Visual Studio Update 3 ships with TypeScript support out of the box, it currently doesn’t ship with TypeScript 3.1, -which you need to develop Angular applications. +While Visual Studio Update 3 ships with TypeScript support out of the box, it currently doesn’t ship with more recent versions of TypeScript, which you need to develop Angular applications. Visual Studio Update 3 自带 TypeScript 支持,但它的 TypeScript 版本不是开发 Angular 应用所需的 3.1。 -To install TypeScript 3.1: +To install the latest version of TypeScript: 要安装 TypeScript 3.1: - * Download and install [TypeScript 3.1 for Visual Studio 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48593), + * Download and install the latest [TypeScript for Visual Studio 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48593), 下载并安装 **[TypeScript 3.1 for Visual Studio 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48593)** - * OR install it with npm: `npm install -g typescript@3.1`. + * OR install it with npm: `npm install -g typescript@latest`. 或通过 npm 安装:`npm install -g typescript@2.2`。 -You can find out more about TypeScript 3.1 support in Visual Studio **[here](https://blogs.msdn.microsoft.com/typescript/announcing-typescript-3-1/)**. +You can find out more about TypeScript support in Visual Studio **[here](https://blogs.msdn.microsoft.com/typescript/announcing-typescript-3-1/)**. 你可以在**[这里](https://blogs.msdn.microsoft.com/typescript/2017/02/22/announcing-typescript-2-2/)**查看更多 Visual Studio 中 TypeScript 3.1 的支持。 @@ -144,11 +142,18 @@ restart it to make sure everything is clean.

Step 1: Download the Angular Getting Started app

-

第一步: 下载“ Angular 快速上手”文件

+

+ Step 1: Create a starter Angular app +

-Go to the final code review in [Getting Started](guide/quickstart) and download the solution app project. These files contain a starter Angular app. +

+ 第一步:创建一个 Angular 的初学者应用 +

-到 [Getting Started](guide/quickstart) 中查看最终代码,并下载这个解决方案的应用项目。这些文件包含一个起步级 Angular 应用。 + + Follow the instructions in [Local Environment Setup](guide/setup-local "Setting up for Local Development") to create a starter Angular app using the CLI command [`ng new`](cli/new). + + 遵循[建立本地环境](guide/setup-local "Setting up for Local Development")中的步骤,使用 CLI 命令 [`ng new`](cli/new) 创建一个 Angular 的初学者应用。

Step 2: Create the Visual Studio ASP.NET project

@@ -183,11 +188,15 @@ no authentication, and no hosting. Pick the template and options appropriate for
-

Step 3: Copy the Angular Getting Started project files into the ASP.NET project folder

+

+ Step 3: Copy the Angular project files into the ASP.NET project folder +

-

第三步: 把“快速上手”的文件复制到 ASP.NET 项目所在的目录

+

+ 第三步: 把这个 Angular 项目中的文件复制到 ASP.NET 项目所在的目录 +

-Copy the files you downloaded from [Getting Started](guide/quickstart) into the folder containing the `.csproj` file. +Copy files from the starter Angular app into the folder containing the `.csproj` file. Include the files in the Visual Studio project as follows: 拷贝从 GitHub 下载的“快速上手”文件到包含 `.csproj` 文件的目录中。按照下面的步骤把它们加到 Visual Studio 中: diff --git a/aio/content/guide/workspace-config.md b/aio/content/guide/workspace-config.md index eb7d983e04..9e9c0f1906 100644 --- a/aio/content/guide/workspace-config.md +++ b/aio/content/guide/workspace-config.md @@ -87,7 +87,7 @@ The following top-level configuration properties are available for each project, 每个项目的 `projects:` 下都有以下顶级配置属性。 - "my-v7-app": { + "my-app": { "root": "", "sourceRoot": "src", "projectType": "application", diff --git a/aio/content/images/bios/kamilmysliwiec.jpg b/aio/content/images/bios/kamilmysliwiec.jpg new file mode 100644 index 0000000000..88c240d57a Binary files /dev/null and b/aio/content/images/bios/kamilmysliwiec.jpg differ diff --git a/aio/content/images/bios/michael-hladky.jpg b/aio/content/images/bios/michael-hladky.jpg new file mode 100644 index 0000000000..8ff1fb0941 Binary files /dev/null and b/aio/content/images/bios/michael-hladky.jpg differ diff --git a/aio/content/images/guide/cli-quickstart/my-first-app.png b/aio/content/images/guide/cli-quickstart/my-first-app.png deleted file mode 100644 index e2346a6111..0000000000 Binary files a/aio/content/images/guide/cli-quickstart/my-first-app.png and /dev/null differ diff --git a/aio/content/images/guide/cli-quickstart/quickstart-sourcemap-explorer.png b/aio/content/images/guide/deployment/quickstart-sourcemap-explorer.png similarity index 100% rename from aio/content/images/guide/cli-quickstart/quickstart-sourcemap-explorer.png rename to aio/content/images/guide/deployment/quickstart-sourcemap-explorer.png diff --git a/aio/content/images/guide/getting-started/cart-page-checkout-form-empty.png b/aio/content/images/guide/getting-started/cart-page-checkout-form-empty.png deleted file mode 100644 index 6d094aa091..0000000000 Binary files a/aio/content/images/guide/getting-started/cart-page-checkout-form-empty.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/cart-page-no-items.png b/aio/content/images/guide/getting-started/cart-page-no-items.png deleted file mode 100644 index 304eda983a..0000000000 Binary files a/aio/content/images/guide/getting-started/cart-page-no-items.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/cart-with-shipping-link.png b/aio/content/images/guide/getting-started/cart-with-shipping-link.png deleted file mode 100644 index 23c7846277..0000000000 Binary files a/aio/content/images/guide/getting-started/cart-with-shipping-link.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/new-project.png b/aio/content/images/guide/getting-started/new-project.png deleted file mode 100644 index 36878c25c7..0000000000 Binary files a/aio/content/images/guide/getting-started/new-project.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/product-details.png b/aio/content/images/guide/getting-started/product-details.png deleted file mode 100644 index 65dd36e726..0000000000 Binary files a/aio/content/images/guide/getting-started/product-details.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/shipping-prices-via-route.png b/aio/content/images/guide/getting-started/shipping-prices-via-route.png deleted file mode 100644 index a7f099d360..0000000000 Binary files a/aio/content/images/guide/getting-started/shipping-prices-via-route.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-angular-icon.png b/aio/content/images/guide/getting-started/stackblitz-angular-icon.png deleted file mode 100644 index 75a2daddc0..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-angular-icon.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-icon-only-small.png b/aio/content/images/guide/getting-started/stackblitz-icon-only-small.png deleted file mode 100644 index c6ff5df84e..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-icon-only-small.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-icon-only.png b/aio/content/images/guide/getting-started/stackblitz-icon-only.png deleted file mode 100644 index 93bce1aaa6..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-icon-only.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-icon-small.png b/aio/content/images/guide/getting-started/stackblitz-icon-small.png deleted file mode 100644 index edffbd1573..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-icon-small.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-icon-smallest.png b/aio/content/images/guide/getting-started/stackblitz-icon-smallest.png deleted file mode 100644 index 3397527510..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-icon-smallest.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/stackblitz-icon.png b/aio/content/images/guide/getting-started/stackblitz-icon.png deleted file mode 100644 index d115fd8545..0000000000 Binary files a/aio/content/images/guide/getting-started/stackblitz-icon.png and /dev/null differ diff --git a/aio/content/images/guide/getting-started/starter-app-components.png b/aio/content/images/guide/getting-started/starter-app-components.png deleted file mode 100644 index 2ad1088661..0000000000 Binary files a/aio/content/images/guide/getting-started/starter-app-components.png and /dev/null differ diff --git a/aio/content/images/guide/cli-quickstart/app-works.png b/aio/content/images/guide/setup-local/app-works.png similarity index 100% rename from aio/content/images/guide/cli-quickstart/app-works.png rename to aio/content/images/guide/setup-local/app-works.png diff --git a/aio/content/images/guide/getting-started/app-components.png b/aio/content/images/guide/start/app-components.png similarity index 100% rename from aio/content/images/guide/getting-started/app-components.png rename to aio/content/images/guide/start/app-components.png diff --git a/aio/content/images/guide/getting-started/buy-alert.png b/aio/content/images/guide/start/buy-alert.png similarity index 100% rename from aio/content/images/guide/getting-started/buy-alert.png rename to aio/content/images/guide/start/buy-alert.png diff --git a/aio/content/images/guide/getting-started/cart-empty-with-shipping-prices.png b/aio/content/images/guide/start/cart-empty-with-shipping-prices.png similarity index 100% rename from aio/content/images/guide/getting-started/cart-empty-with-shipping-prices.png rename to aio/content/images/guide/start/cart-empty-with-shipping-prices.png diff --git a/aio/content/images/guide/getting-started/cart-page-full.png b/aio/content/images/guide/start/cart-page-full.png similarity index 100% rename from aio/content/images/guide/getting-started/cart-page-full.png rename to aio/content/images/guide/start/cart-page-full.png diff --git a/aio/content/images/guide/getting-started/cart-with-items-and-form.png b/aio/content/images/guide/start/cart-with-items-and-form.png similarity index 100% rename from aio/content/images/guide/getting-started/cart-with-items-and-form.png rename to aio/content/images/guide/start/cart-with-items-and-form.png diff --git a/aio/content/images/guide/getting-started/cart-works.png b/aio/content/images/guide/start/cart-works.png similarity index 100% rename from aio/content/images/guide/getting-started/cart-works.png rename to aio/content/images/guide/start/cart-works.png diff --git a/aio/content/images/guide/getting-started/generate-component.png b/aio/content/images/guide/start/generate-component.png similarity index 100% rename from aio/content/images/guide/getting-started/generate-component.png rename to aio/content/images/guide/start/generate-component.png diff --git a/aio/content/images/guide/getting-started/new-app.png b/aio/content/images/guide/start/new-app.png similarity index 100% rename from aio/content/images/guide/getting-started/new-app.png rename to aio/content/images/guide/start/new-app.png diff --git a/aio/content/images/guide/getting-started/product-alert-button.png b/aio/content/images/guide/start/product-alert-button.png similarity index 100% rename from aio/content/images/guide/getting-started/product-alert-button.png rename to aio/content/images/guide/start/product-alert-button.png diff --git a/aio/content/images/guide/getting-started/product-alert-notification.png b/aio/content/images/guide/start/product-alert-notification.png similarity index 100% rename from aio/content/images/guide/getting-started/product-alert-notification.png rename to aio/content/images/guide/start/product-alert-notification.png diff --git a/aio/content/images/guide/getting-started/product-details-buy.png b/aio/content/images/guide/start/product-details-buy.png similarity index 100% rename from aio/content/images/guide/getting-started/product-details-buy.png rename to aio/content/images/guide/start/product-details-buy.png diff --git a/aio/content/images/guide/getting-started/product-details-routed.png b/aio/content/images/guide/start/product-details-routed.png similarity index 100% rename from aio/content/images/guide/getting-started/product-details-routed.png rename to aio/content/images/guide/start/product-details-routed.png diff --git a/aio/content/images/guide/getting-started/product-details-works.png b/aio/content/images/guide/start/product-details-works.png similarity index 100% rename from aio/content/images/guide/getting-started/product-details-works.png rename to aio/content/images/guide/start/product-details-works.png diff --git a/aio/content/images/guide/getting-started/shipping-prices.png b/aio/content/images/guide/start/shipping-prices.png similarity index 100% rename from aio/content/images/guide/getting-started/shipping-prices.png rename to aio/content/images/guide/start/shipping-prices.png diff --git a/aio/content/images/guide/getting-started/template-syntax-product-anchor.png b/aio/content/images/guide/start/template-syntax-product-anchor.png similarity index 100% rename from aio/content/images/guide/getting-started/template-syntax-product-anchor.png rename to aio/content/images/guide/start/template-syntax-product-anchor.png diff --git a/aio/content/images/guide/getting-started/template-syntax-product-description.png b/aio/content/images/guide/start/template-syntax-product-description.png similarity index 100% rename from aio/content/images/guide/getting-started/template-syntax-product-description.png rename to aio/content/images/guide/start/template-syntax-product-description.png diff --git a/aio/content/images/guide/getting-started/template-syntax-product-names.png b/aio/content/images/guide/start/template-syntax-product-names.png similarity index 100% rename from aio/content/images/guide/getting-started/template-syntax-product-names.png rename to aio/content/images/guide/start/template-syntax-product-names.png diff --git a/aio/content/images/guide/getting-started/template-syntax-product-share-alert.png b/aio/content/images/guide/start/template-syntax-product-share-alert.png similarity index 100% rename from aio/content/images/guide/getting-started/template-syntax-product-share-alert.png rename to aio/content/images/guide/start/template-syntax-product-share-alert.png diff --git a/aio/content/images/guide/getting-started/template-syntax-product-share-button.png b/aio/content/images/guide/start/template-syntax-product-share-button.png similarity index 100% rename from aio/content/images/guide/getting-started/template-syntax-product-share-button.png rename to aio/content/images/guide/start/template-syntax-product-share-button.png diff --git a/aio/content/marketing/analytics.md b/aio/content/marketing/analytics.md index a6b495c7aa..e6d2f093d7 100644 --- a/aio/content/marketing/analytics.md +++ b/aio/content/marketing/analytics.md @@ -15,8 +15,7 @@ include the following information: - For Schematics commands (add, generate, new and update), a list of whitelisted flags. - For build commands (build, serve), the number and size of bundles (initial and lazy), compilation units, the time it took to build and rebuild, and basic Angular-specific - API usage. *This data is collected only if usage analytics gathering is enabled for - the project.* + API usage. - Error code of exceptions and crash data. No stack trace is collected. Only Angular owned and developed schematics and builders are reported. Third-party schematics and @@ -26,9 +25,6 @@ builders do not send data to the Angular Team. When installing the Angular CLI or upgrading an existing version, you are prompted to allow global collection of usage statistics. If you say no or skip the prompt, no data is collected. -The first time a command affecting the project is run, you are prompted to allow collection of data -related to the project. If you say no or skip the prompt, no data is collected for that project. - Starting with version 8, we added the `analytics` command to the CLI. You can change your opt-in decision at any time using this command. @@ -38,9 +34,6 @@ To disable analytics gathering, run the following command: ```bash # Disable all usage analytics. ng analytics off - -# Disable project-specific usage analytics. -ng analytics project off ``` ### Enabling usage analytics @@ -49,9 +42,6 @@ To enable usage analytics, run the following command: ```bash # Enable all usage analytics. ng analytics on - -# Enable project-specific usage analytics. -ng analytics project on ``` ### Prompting @@ -60,7 +50,4 @@ To prompt the user again about usage analytics, run the following command: ```bash # Prompt for all usage analytics. ng analytics prompt - -# Prompt for project-specific usage analytics. -ng analytics project prompt ``` diff --git a/aio/content/marketing/contributors.json b/aio/content/marketing/contributors.json index 9d1be15272..27dce186e8 100644 --- a/aio/content/marketing/contributors.json +++ b/aio/content/marketing/contributors.json @@ -327,6 +327,14 @@ "bio": "Owner and trainer at Ultimate Angular. Lives in England, UK. Has a love for teaching, OSS and speaking at conferences. Google Developer Expert for Web Technologies and Angular.", "groups": ["GDE"] }, + "michaelhladky": { + "name": "Michael Hladky", + "picture": "michael-hladky.jpg", + "twitter": "Michael_Hladky", + "website": "https://github.com/BioPhoton", + "bio": "Michael is a self employed trainer, consultant and developer with the focus on Angular, and located in Vienna, Austria. He gives workshops on Angular, RxJS and Ionic. As Google Developer Expert, founder of Angular-Austria-Association and Angular-Vienna meetup, and speaker he is an active part of the community.", + "groups": ["GDE"] + }, "michaelprentice": { "name": "Michael Prentice", "picture": "michaelprentice.jpg", @@ -793,5 +801,13 @@ "bio": "Andrew is a software engineer at Google on the Angular Core team.", "groups": ["Angular"], "lead": "kara" + }, + "kamilmysliwiec": { + "name": "Kamil Mysliwiec", + "picture": "kamilmysliwiec.jpg", + "twitter": "kammysliwiec", + "website": "https://github.com/kamilmysliwiec", + "bio": "Kamil Mysliwiec is a software engineer truly passionate about Web Technologies. Creator of NestJS, Co-Founder of Trilon.io, speaker, and trainer.", + "groups": ["GDE"] } } diff --git a/aio/content/marketing/docs.md b/aio/content/marketing/docs.md index 4abad777f2..f19b3942d3 100644 --- a/aio/content/marketing/docs.md +++ b/aio/content/marketing/docs.md @@ -1,77 +1,80 @@ -

What is Angular?

+

Introduction to the Angular Docs

什么是 Angular?

-Angular is a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges. Angular empowers developers to build applications that live on the web, mobile, or the desktop. +These Angular docs help you learn and use the Angular platform and framework, from your first app to optimizing complex enterprise apps. +Tutorials and guides include downloadable example to accelerate your projects. -Angular 是一个开发平台。它能帮你更轻松的构建 Web 应用。Angular 集声明式模板、依赖注入、端到端工具和一些最佳实践于一身,为你解决开发方面的各种挑战。Angular 为开发者提升构建 Web、手机或桌面应用的能力。 +这份 Angular 文档会帮助你学习和使用 Angular 平台&框架,从你的第一个应用开始,一直到优化复杂的企业应用。 +这些教程和指南中都包含可下载的范例,以加速你的学习。 + ## Assumptions ## 基本假设 -This documentation assumes that you are already familiar with -[JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript"), -and some of the tools from the -[latest standards](https://babeljs.io/learn-es2015/ "Latest JavaScript standards") such as -[classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") -and [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules"). -The code samples are written using [TypeScript](https://www.typescriptlang.org/ "TypeScript"). -Most Angular code can be written with just the latest JavaScript, -using [types](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") for dependency injection, -and using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators") for metadata. +These docs assume that you are already familiar with HTML, CSS, [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript"), +and some of the tools from the [latest standards](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources "Latest JavaScript standards"), such as [classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") and [modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules"). +The code samples are written using [TypeScript](https://www.typescriptlang.org/ "TypeScript"). +Most Angular code can be written with just the latest JavaScript, using [types](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") for dependency injection, and using [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators") for metadata. -本文档假设你已经熟悉了 [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript") 和来自 [最新标准](https://babeljs.io/learn-es2015/ "Latest JavaScript standards") 的一些知识,比如 [类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") 和 [模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules")。 + +本文档假设你已经熟悉了 HTML,CSS,[JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript "Learn JavaScript") 和来自 [最新标准](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources "Latest JavaScript standards") 的一些知识,比如 [类](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes "ES2015 Classes") 和 [模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import "ES2015 Modules")。 下列代码范例都是用最新版本的 [TypeScript](https://www.typescriptlang.org/ "TypeScript") 写的。 大多数 Angular 代码都只能用最新的 JavaScript 编写,它会用 [类型](https://www.typescriptlang.org/docs/handbook/classes.html "TypeScript Types") 实现依赖注入,还会用[装饰器](https://www.typescriptlang.org/docs/handbook/decorators.html "Decorators")来提供元数据。 -## Feedback +## Feedback ## 反馈 -You can sit with us! +

You can sit with us!

-你也可以和我们一起做贡献! +

你也可以和我们一起做贡献!

-You can file documentation -[issues](https://github.com/angular/angular/issues "Angular Github issues") and create +We want to hear from you. [Report problems or submit suggestions for future docs.](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form") + +我们希望听到你的声音![欢迎报告问题或为文档的未来提交建议](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form")。 + +Contribute to Angular docs by creating [pull requests](https://github.com/angular/angular/pulls "Angular Github pull requests") on the Angular Github repository. -The [contributing guide](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "Contributing guide") -will help you contribute to the community. -Our community values respectful, supportive communication. -Please consult and adhere to the -[code of conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "contributor code of conduct"). +See [Contributing to Angular](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "Contributing guide") +for information about submission guidelines. -你可以到 Angular 在 Github 上的仓库中提出文档方面的[问题](https://github.com/angular/angular/issues "Angular Github issues"),并创建[Pull Requests](https://github.com/angular/angular/pulls "Angular Github pull requests")。 +请到 Github 上的仓库中创建 [Pull Requests](https://github.com/angular/angular/pulls "Angular Github pull requests") 来为 Angular 文档做出贡献。 [贡献者指南](https://github.com/angular/angular/blob/master/CONTRIBUTING.md "贡献者指南")将会帮助你更好的为社区做贡献。 -我们社区的价值观是互相尊重、互相支持。参见[社区行为规范](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "contributor code of conduct")。 + +Our community values respectful, supportive communication. +Please consult and adhere to the [Code of Conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "Contributor code of conduct"). + +我们的社区提倡相互尊重、相互支持。 +参见[社区行为规范](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md "contributor code of conduct")。 diff --git a/aio/content/marketing/features.html b/aio/content/marketing/features.html index bc19270964..e03a6716b9 100755 --- a/aio/content/marketing/features.html +++ b/aio/content/marketing/features.html @@ -114,7 +114,7 @@
diff --git a/aio/content/marketing/index.html b/aio/content/marketing/index.html index 200e4583b8..5d88657eb8 100755 --- a/aio/content/marketing/index.html +++ b/aio/content/marketing/index.html @@ -15,7 +15,7 @@
一套框架,多种平台
移动端 & 桌面端
- 快速上手 + 快速上手
@@ -99,9 +99,9 @@
- +
- Angular quickstart + Getting Started with Angular
立即开始

构建你的 Angular 应用

diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 2750b8304d..34ae97764e 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -51,9 +51,9 @@ "SideNav": [ { "url": "docs", - "title": "文档", - "tooltip": "Angular 开发文档", - "hidden": true + "title": "简介", + "tooltip": "Angular 文档简介", + "hidden": false }, { "url": "guide/docs-style-guide", @@ -66,87 +66,87 @@ "tooltip": "通过构建第一个 Angular 应用来学习基础知识", "children": [ { - "url": "getting-started", + "url": "start", "title": "你的第一个应用", "tooltip": "介绍 Angular 的组件模型、模板语法和组件通讯" }, { - "url": "getting-started/routing", + "url": "start/routing", "title": "路由", "tooltip": "介绍如何使用浏览器的 URL 在组件之间进行路由" }, { - "url": "getting-started/data", + "url": "start/data", "title": "管理数据", "tooltip": "介绍服务以及如何访问外部数据" }, { - "url": "getting-started/forms", + "url": "start/forms", "title": "表单", "tooltip": "学习如何使用表单从用户那里获取并管理数据" }, { - "url": "getting-started/deployment", + "url": "start/deployment", "title": "部署", "tooltip": "通过你的应用放到 Firebase 或自己的服务器上来把它分享给外界" } ] }, { - "url": "guide/quickstart", - "title": "快速搭建环境", + "url": "guide/setup-local", + "title": "搭建环境", "tooltip": "使用 Angular CLI 搭建本地开发环境简介" }, { - "title": "教程:英雄指南", - "tooltip": "此《英雄指南》教程会带你用 TypeScript 一步步创建一个 Angular 应用。", + "title": "基本原理", + "tooltip": "Angular 的基本原理", "children": [ { - "url": "tutorial", - "title": "简介", - "tooltip": "《英雄指南》教程简介" + "title": "英雄指南", + "tooltip": "英雄指南是这里大量 Angular 范例的基础", + "children": [ + { + "url": "tutorial", + "title": "简介", + "tooltip": "《英雄指南》教程简介" + }, + { + "url": "tutorial/toh-pt0", + "title": "应用的“外壳”", + "tooltip": "创建应用的外壳" + }, + { + "url": "tutorial/toh-pt1", + "title": "1. 英雄编辑器", + "tooltip": "第一部分:构建一个简单的英雄编辑器" + }, + { + "url": "tutorial/toh-pt2", + "title": "2. 显示英雄列表", + "tooltip": "第二部分:构建一个主从结构的页面,用于展现英雄列表。" + }, + { + "url": "tutorial/toh-pt3", + "title": "3. 主从组件", + "tooltip": "第三部分:把主从结构的页面重构成多个组件。" + }, + { + "url": "tutorial/toh-pt4", + "title": "4. 服务", + "tooltip": "第四部分:创建一个可复用的服务来管理英雄数据。" + }, + { + "url": "tutorial/toh-pt5", + "title": "5. 路由", + "tooltip": "第五部分:添加 Angular 路由器,并且学习在视图之间导航。" + }, + { + "url": "tutorial/toh-pt6", + "title": "6. HTTP", + "tooltip": "第六部分:通过 HTTP 来获取并保存英雄数据。" + } + ] }, - { - "url": "tutorial/toh-pt0", - "title": "应用的“外壳”", - "tooltip": "创建应用的外壳" - }, - { - "url": "tutorial/toh-pt1", - "title": "1. 英雄编辑器", - "tooltip": "第一部分:构建一个简单的英雄编辑器" - }, - { - "url": "tutorial/toh-pt2", - "title": "2. 显示英雄列表", - "tooltip": "第二部分:构建一个主从结构的页面,用于展现英雄列表。" - }, - { - "url": "tutorial/toh-pt3", - "title": "3. 主从组件", - "tooltip": "第三部分:把主从结构的页面重构成多个组件。" - }, - { - "url": "tutorial/toh-pt4", - "title": "4. 服务", - "tooltip": "第四部分:创建一个可复用的服务来管理英雄数据。" - }, - { - "url": "tutorial/toh-pt5", - "title": "5. 路由", - "tooltip": "第五部分:添加 Angular 路由器,并且学习在视图之间导航。" - }, - { - "url": "tutorial/toh-pt6", - "title": "6. HTTP", - "tooltip": "第六部分:通过 HTTP 来获取并保存英雄数据。" - } - ] - }, - { - "title": "核心知识", - "tooltip": "学习 Angular 的核心知识", - "children": [ { "title": "架构", "tooltip": "Angular 应用的基本构造块。", @@ -564,6 +564,11 @@ } ] }, + { + "url": "guide/cli-builder", + "title": "CLI Builders", + "tooltip": "Using builders to customize Angular CLI." + }, { "url": "guide/ivy", "title": "Angular Ivy(常春藤)", @@ -577,35 +582,15 @@ ] }, { - "title": "环境搭建与部署", - "tooltip": "构建、测试、部署环境、工具以及配置信息。", + "title": "开发工作流", + "tooltip": "关于构建、测试和部署的信息", "children": [ { "url": "guide/setup", - "title": "搭建本地开发环境", - "tooltip": "安装 Angular 快速起步种子工程,以便在你的电脑上更快、更高效的开发。", + "title": "升级的准备工作", + "tooltip": "How to set up the Angular QuickStart seed in the context of upgrading from AngularJS.", "hidden": true }, - { - "url": "guide/file-structure", - "title": "项目文件结构", - "tooltip": "Angular 工作区在文件系统中是怎样的。" - }, - { - "url": "guide/workspace-config", - "title": "工作区配置", - "tooltip": "\"angular.json\" 包含供 CLI 命令使用的工作区和项目默认配置。" - }, - { - "url": "guide/npm-packages", - "title": "npm 包", - "tooltip": "开发期间和运行期间所需的 npm 包的说明。" - }, - { - "url": "guide/typescript-configuration", - "title": "TypeScript 配置", - "tooltip": "给 Angular 开发者的 TypeScript 配置。" - }, { "url": "guide/aot-compiler", "title": "预先(AOT)编译", @@ -631,14 +616,9 @@ "title": "发布", "tooltip": "了解如何部署 Angular 应用。" }, - { - "url": "guide/browser-support", - "title": "浏览器支持", - "tooltip": "浏览器支持与腻子脚本指南。" - }, { "title": "开发工具集成", - "tooltip": "整合开发环境和工具。", + "tooltip": "与开发环境和工具集成起来", "children": [ { "url": "guide/language-service", @@ -655,6 +635,37 @@ } ] }, + { + "title": "配置", + "tooltip": "工作空间与项目的结构,及其配置文件。", + "children": [ + { + "url": "guide/file-structure", + "title": "项目文件结构", + "tooltip": "Angular 工作区在文件系统中是怎样的。" + }, + { + "url": "guide/workspace-config", + "title": "工作区配置", + "tooltip": "\"angular.json\" 包含供 CLI 命令使用的工作区和项目默认配置。" + }, + { + "url": "guide/npm-packages", + "title": "npm 包", + "tooltip": "开发期间和运行期间所需的 npm 包的说明。" + }, + { + "url": "guide/typescript-configuration", + "title": "TypeScript 配置", + "tooltip": "给 Angular 开发者的 TypeScript 配置。" + }, + { + "url": "guide/browser-support", + "title": "浏览器支持", + "tooltip": "浏览器支持与腻子脚本指南。" + } + ] + }, { "title": "发布信息", "tooltip": "Angular 的版本发布实践、更新与升级。", @@ -816,7 +827,7 @@ { "url": "events", "title": "活动", - "tooltip": "Post issues and suggestions on github." + "tooltip": "Angular events around the world." }, { "url": "http://www.meetup.com/topics/angularjs/", @@ -859,21 +870,25 @@ } ], "docVersions": [ + { + "title": "v7", + "url": "https://v7.angular.io/" + }, { "title": "v6", - "url": "https://v6.angular.io" + "url": "https://v6.angular.io/" }, { "title": "v5", - "url": "https://v5.angular.io" + "url": "https://v5.angular.io/" }, { "title": "v4", - "url": "https://v4.angular.io" + "url": "https://v4.angular.io/" }, { "title": "v2", - "url": "https://v2.angular.io" + "url": "https://v2.angular.io/" } ] } diff --git a/aio/content/getting-started/data.md b/aio/content/start/data.md similarity index 93% rename from aio/content/getting-started/data.md rename to aio/content/start/data.md index e76ee735ea..b26a428efd 100644 --- a/aio/content/getting-started/data.md +++ b/aio/content/start/data.md @@ -3,7 +3,7 @@ # 管理数据 -At the end of [Routing](getting-started/routing "Getting Started: Routing"), the online store application has a product catalog with two views: a product list and product details. +At the end of [Routing](start/routing "Getting Started: Routing"), the online store application has a product catalog with two views: a product list and product details. Users can click on a product name from the list to see details in a new view, with a distinct URL (route). 在[路由](getting-started/routing "入门:路由")的末尾,本应用实现了一个包含两个视图的商品名录:商品列表和商品详情。用户点击清单中的某个商品名称,就会在新视图中看到具有显著 URL(路由)的详情页。 @@ -22,7 +22,7 @@ In this section, you'll create the shopping cart. You'll: 添加一个购物车组件,它会显示你添加到购物车中的商品。 -* Add a shipping component, which retrieves shipping prices for the items in the cart by using Angular's HttpClient to retrieve shipping data from a `.json` file. +* Add a shipping component, which retrieves shipping prices for the items in the cart by using Angular's `HttpClient` to retrieve shipping data from a `.json` file. 添加一个配送组件,它会使用 Angular 的 HttpClient 从 `.json` 文件中检索配送数据来取得购物车中这些商品的运费。 @@ -62,7 +62,7 @@ You'll also set up a cart service to store information about products in the car
-Later, in the [Forms](getting-started/forms "Getting Started: Forms") part of this tutorial, this cart service also will be accessed from the page where the user checks out. +Later, in the [Forms](start/forms "Getting Started: Forms") part of this tutorial, this cart service also will be accessed from the page where the user checks out. 稍后,在本教程的[表单](getting-started/forms "入门:表单")部分,也会从用户的结账页面中访问这个 购物车服务。 @@ -80,7 +80,7 @@ Later, in the [Forms](getting-started/forms "Getting Started: Forms") part of th 生成购物车服务。 - 1. Right click on the `app` folder, choose `Angular Generator`, and choose `**Service**`. Name the new service `cart`. + 1. Right click on the `app` folder, choose `Angular Generator`, and choose `Service`. Name the new service `cart`. 右键单击 `app` 文件夹,选择 `Angular Generator`,然后选择 `**Service**`。把新的服务命名为 `cart`。 @@ -222,7 +222,7 @@ When the "Buy" button is clicked, you'll use the cart service to add the current
- Display details for selected product with a Buy button + Display details for selected product with a Buy button
1. Click the "Buy" button. The product is added to the stored list of items in the cart, and a message is displayed. @@ -231,7 +231,7 @@ When the "Buy" button is clicked, you'll use the cart service to add the current
- Display details for selected product with a Buy button + Display details for selected product with a Buy button
## Create the cart page @@ -311,7 +311,7 @@ We'll create the cart page in two steps:
- Display cart page before customizing + Display cart page before customizing
### Display the cart items @@ -420,7 +420,7 @@ Services can be used to share data across components:
- Cart page with products added + Cart page with products added
@@ -469,7 +469,7 @@ In this section, you'll use the HTTP client to retrieve shipping prices from an ### 预定义的配送数据 -For the purpose of this Getting Started, we have provided shipping data in `assets/shipping.json`. +For the purpose of this Getting Started guide, we have provided shipping data in `assets/shipping.json`. You'll use this data to add shipping prices for items in the cart. 为了满足本“入门指南”的需求,我们在 `assets/shipping.json` 中提供了配送数据。你可以利用这些数据为购物车中的商品添加运费。 @@ -588,7 +588,7 @@ Here you'll define the `get()` method that will be used.
-Learn more: See the [HttpClient guide](guide/http "HttpClient guide") for more information about Angular's HttpClient. +Learn more: See the [HttpClient guide](guide/http "HttpClient guide") for more information about Angular's `HttpClient`. 要了解关于 Angular HttpClient 的更多信息,请参阅[HttpClient 指南](guide/http "HttpClient 指南")。 @@ -715,7 +715,7 @@ Now that your app can retrieve shipping data, you'll create a shipping component
- Cart with link to shipping prices + Cart with link to shipping prices
Click on the link to navigate to the shipping prices. @@ -724,7 +724,7 @@ Now that your app can retrieve shipping data, you'll create a shipping component
- Display shipping prices + Display shipping prices
## Next steps @@ -742,11 +742,11 @@ To continue exploring Angular, choose either of the following options: 要继续探索 Angular,请选择下列选项之一: -* [Continue to the "Forms" section](getting-started/forms "Getting Started: Forms") to finish the app by adding the shopping cart page and a form-based checkout feature. You'll create a form to collect user information as part of checkout. +* [Continue to the "Forms" section](start/forms "Getting Started: Forms") to finish the app by adding the shopping cart page and a form-based checkout feature. You'll create a form to collect user information as part of checkout. [继续浏览“表单”部分](getting-started/forms "入门:表单"),通过添加购物车页面和基于表单的结帐功能来完成该应用。你还可以创建一个表单来收集用户信息,作为结账过程的一部分。 -* [Skip ahead to the "Deployment" section](getting-started/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development. +* [Skip ahead to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server. [跳到“部署”部分,](getting-started/deployment "入门:部署")把你的应用部署到 Firebase 或转成本地开发。 diff --git a/aio/content/getting-started/deployment.md b/aio/content/start/deployment.md similarity index 77% rename from aio/content/getting-started/deployment.md rename to aio/content/start/deployment.md index 424cff719f..dbbdaff178 100644 --- a/aio/content/getting-started/deployment.md +++ b/aio/content/start/deployment.md @@ -10,54 +10,44 @@ To deploy your application, you have to compile it, and then host the JavaScript
-Whether you came here directly from [Your First App](getting-started "Getting Started: Your First App"), or completed the entire online store application through the [Routing](getting-started/routing "Getting Started: Routing"), [Managing Data](getting-started/data "Getting Started: Managing Data"), and [Forms](getting-started/forms "Getting Started: Forms") sections, you have an application that you can deploy by following the instructions in this section. +Whether you came here directly from [Your First App](start "Getting Started: Your First App"), or completed the entire online store application through the [Routing](start/routing "Getting Started: Routing"), [Managing Data](start/data "Getting Started: Managing Data"), and [Forms](start/forms "Getting Started: Forms") sections, you have an application that you can deploy by following the instructions in this section. 无论你是从[你的第一个应用](getting-started "入门:你的第一个应用")直接来到这里,还是经过[路由](getting-started/routing "入门:路由")、[管理数据](getting-started/data "入门:管理数据")和[表单](getting-started/forms "入门:表单")部分,完成了整个在线商店应用之后来到这里,都可以按照本节中的说明进行部署。
-## Deploying from StackBlitz +## Share your application ## 从 StackBlitz 开始部署 -StackBlitz allows you to publish your Angular app directly to Firebase from your project. The steps below outline how to deploy it quickly without setting up your own hosting environment. +StackBlitz projects are public by default, allowing you to share your Angular app via the project URL. Keep in mind that this is a great way to share ideas and prototypes, but it is not intended for production hosting. StackBlitz 允许你从项目中把 Angular 应用直接发布到 Firebase。下面的步骤简要描述了如何在不必设置自己的开发环境的情况下快速部署它。 +1. In your StackBlitz project, make sure you have forked or saved your project. -1. In your StackBlitz project, in the left menu bar, click the `Firebase` icon. + 在你的 StackBlitz 项目中,请先确保你已经分支或保存了项目。 - 在 StackBlitz 项目的左侧菜单栏中,点击 `Firebase` 图标。 +1. In the preview pane, you should see a URL that looks like `https://.stackblitz.io`. -1. If you don’t have a `Firebase` account, visit the [Firebase](https://firebase.google.com/ "Firebase web site") to sign up for a free hosting account. + 在预览窗格,你会看到一个形如 `https://.stackblitz.io` 的 URL。 - 如果你还没有 `Firebase` 帐户,请访问 [Firebase](https://firebase.google.com/ "Firebase 网站") 注册一个免费的托管帐户。 +1. Share this URL with a friend or colleague. -1. Click the `Sign into Google` button and follow the prompts to give `StackBlitz` access your `Firebase` projects + 把这个 URL 共享给朋友或同事。 - 点击 `Sign into Google` 按钮,然后按照提示允许 `StackBlitz` 访问你的 `Firebase` 项目 - -1. Select the `project` where you wish to deploy your application. - - 选择要部署应用的项目(`project`)。 - -1. Click the `Deploy` button to deploy your application. - - 单击 `Deploy` 按钮,部署应用。 - -1. After the deployment completes, click the `Open live site` link to view your app live. - - 部署完成后,点击 `Open live site` 链接,立即查看你的应用。 +1. Users that visit your URL will see a development server start up, and then your application will load. + 访问你的 URL 的用户会看到启动了一个开发服务器,然后就会加载你的应用。 ## Building locally ## 本地构建 -To build your application locally, you will need to download the source code from your StackBlitz project. Click the `Download Project` icon in the left menu across from `Project` to download your files. +To build your application locally or for production, you will need to download the source code from your StackBlitz project. Click the `Download Project` icon in the left menu across from `Project` to download your files. 要在本地构建应用,你需要从 StackBlitz 项目中下载源代码。单击左侧菜单中的 `Download Project` 图标以下载文件。 @@ -109,7 +99,7 @@ This will produce the files that you need to deploy. #### 托管已构建的项目 -The files in the `dist/my-project-name` folder are static and can be hosted on any web server capable of serving files (node, Java, .NET) or any backend (Firebase, Google Cloud, App Engine, others). +The files in the `dist/my-project-name` folder are static and can be hosted on any web server capable of serving files (Node, Java, .NET) or any backend (Firebase, Google Cloud, App Engine, others). `dist/my-project-name` 文件夹中的文件都是静态的,可以托管在任何能够提供文件服务的 Web 服务器上(node,Java,.NET),也可以是任何后端(Firebase,Google Cloud,App Engine 等)。 @@ -170,7 +160,7 @@ Learn more about development and distribution of your application in the [Buildi ## 加入我们的社区 -You are now an Angular developer! [Share this moment](https://twitter.com/intent/tweet?url=https://next.angular.io/getting-started&text=I%20just%20finished%20the%20Angular%20Getting%20Started%20Tutorial "Angular on Twitter"), tell us what you thought of this Getting Started, or submit [suggestions for future editions](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form"). +You are now an Angular developer! [Share this moment](https://twitter.com/intent/tweet?url=https://angular.io/start&text=I%20just%20finished%20the%20Angular%20Getting%20Started%20Tutorial "Angular on Twitter"), tell us what you thought of this Getting Started, or submit [suggestions for future editions](https://github.com/angular/angular/issues/new/choose "Angular GitHub repository new issue form"). 你现在是一位 Angular 的开发者了![分享这一刻](https://twitter.com/intent/tweet?url=https://next.angular.io/getting-started&text=I%20just%20finished%20the%20Angular%20Getting%20Started%20Tutorial "Angular on Twitter"),告诉我们你对这份“入门文档”的看法,或者[为今后的版本](https://github.com/angular/angular/issues/new/choose "Angular GitHub 存储库中的新问题表单")提交[建议](https://github.com/angular/angular/issues/new/choose "Angular GitHub 存储库中的新问题表单")。 diff --git a/aio/content/getting-started/forms.md b/aio/content/start/forms.md similarity index 94% rename from aio/content/getting-started/forms.md rename to aio/content/start/forms.md index 1e58d9bae0..a9c2cb5169 100644 --- a/aio/content/getting-started/forms.md +++ b/aio/content/start/forms.md @@ -3,7 +3,7 @@ # 表格 -At the end of [Managing Data](getting-started/data "Getting Started: Managing Data"), the online store application has a product catalog and a shopping cart. +At the end of [Managing Data](start/data "Getting Started: Managing Data"), the online store application has a product catalog and a shopping cart. 当[管理数据](getting-started/data "入门:管理数据")结束时,这个在线商店应用有了一个商品名录和一个购物车。 @@ -153,7 +153,7 @@ After putting a few items in the cart, users can now review their items, enter n
- Cart page with checkout form + Cart page with checkout form
## Next steps @@ -166,7 +166,7 @@ Congratulations! You have a complete online store application with a product cat 恭喜!你有了一个完整的在线商店应用,它具有商品名录,购物车和结账功能。 -[Continue to the "Deployment" section](getting-started/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development. +[Continue to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server. [继续浏览“部署”部分,](getting-started/deployment "入门:部署")把你的应用部署到 Firebase,或者转成本地开发。 diff --git a/aio/content/getting-started/index.md b/aio/content/start/index.md similarity index 93% rename from aio/content/getting-started/index.md rename to aio/content/start/index.md index 690f06665f..ebf4469158 100644 --- a/aio/content/getting-started/index.md +++ b/aio/content/start/index.md @@ -21,7 +21,7 @@ You don't need to install anything: you'll build the app using the [StackBlitz](
你是 Web 开发的新手吗?
-You'll find many resources to complement the Angular docs. Mozilla's MDN docs include both [HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML "Learning HTML: Guides and tutorials") and [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript "JavaScript") introductions. [TypeScript's docs](https://www.typescriptlang.org/docs/home.html "TypeScript documentation") include a 5-minute tutorial. Various online course platforms, such as [Udemy](http://www.udemy.com "Udemy online courses") and [Codeacademy](https://www.codecademy.com/ "Codeacademy online courses"), also cover web development basics. +You'll find many resources to complement the Angular docs. Mozilla's MDN docs include both [HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML "Learning HTML: Guides and tutorials") and [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript "JavaScript") introductions. [TypeScript's docs](https://www.typescriptlang.org/docs/home.html "TypeScript documentation") include a 5-minute tutorial. Various online course platforms, such as [Udemy](http://www.udemy.com "Udemy online courses") and [Codecademy](https://www.codecademy.com/ "Codeacademy online courses"), also cover web development basics. 你可以找到很多资源作为 Angular 文档的补充。Mozilla 的 MDN 文档同时包含了 [HTML](https://developer.mozilla.org/en-US/docs/Learn/HTML "学习 HTML:指南和教程") 和 [JavaScript 的](https://developer.mozilla.org/en-US/docs/Web/JavaScript "JavaScript") 介绍。[TypeScript 的文档](https://www.typescriptlang.org/docs/home.html "TypeScript 文档")中包含一个 5 分钟教程。各种在线课程平台,比如 [Udemy](http://www.udemy.com "Udemy 在线课程") 和 [Codeacademy](https://www.codecademy.com/ "Codeacademy 在线课程"),也涵盖了 Web 开发的一些基础知识。 @@ -45,13 +45,8 @@ We've seeded this particular app with a top bar—containing the store name 用 StackBlitz 创建一个入门级 Angular 应用。我们已经为这个应用添加了一个包含商店名称和结帐图标的顶栏,以及商品列表的标题。 - - -
- Starter online store app + Starter online store app
@@ -123,10 +118,9 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- `*ngFor` is a "structural directive". Structural directives shape or reshape the DOM's structure, typically by adding, removing, and manipulating the elements to which they are attached. Any directive with an * is a structural directive. + `*ngFor` is a "structural directive". Structural directives shape or reshape the DOM's structure, typically by adding, removing, and manipulating the elements to which they are attached. Any directive with an `*` is a structural directive. `*ngFor` 是一个 "结构型指令"。结构型指令会通过添加、删除和操纵它们的宿主元素等方式塑造或重塑 DOM 的结构。任何带有 * 的指令都是结构型指令。 -
1. To display the names of the products, use the interpolation syntax {{ }}. Interpolation renders a property's value as text. Inside the `
`, add an `

` heading to display the interpolation of the product's name property: @@ -143,7 +137,7 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- Product names added to list + Product names added to list
1. In the final app, each product name will be a link to product details. Add the anchor now, and set the anchor's title to be the product's name by using the property binding [ ] syntax, as shown below: @@ -164,7 +158,7 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- Product name anchor text is product name property + Product name anchor text is product name property
@@ -182,7 +176,7 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- Product descriptions added to list + Product descriptions added to list
1. Add a button so users can share a product with friends. Bind the button's `click` event to the `share()` event that we defined for you (in `product-list.component.ts`). Event binding is done by using ( ) around the event, as shown below: @@ -199,7 +193,7 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- Share button added for each product + Share button added for each product
Test the "Share" button: @@ -208,7 +202,7 @@ Angular 的模板语法扩展了 HTML 和 JavaScript。在本节中,你将通
- Alert box indicating product has been shared + Alert box indicating product has been shared
The app now has a product list and sharing feature. @@ -219,15 +213,15 @@ In the process, you've learned to use five common features of Angular's template * `*ngFor` * `*ngIf` -* Interpolation {{ }} +* Interpolation `{{ }}` 插值表达式 `{{}}` -* Property binding [ ] +* Property binding `[ ]` 属性绑定 `[]` -* Event binding ( ) +* Event binding `( )` 事件绑定 `()` @@ -301,7 +295,7 @@ Currently, our app has three components:
- Online store with three components + Online store with three components
* `app-root` (orange box) is the application shell. This is the first component to load, and the parent of all other components. You can think of it as the base page. @@ -361,7 +355,7 @@ We're going to create a new alert feature. The alert feature will take a product
- StackBlitz command to generate component + StackBlitz command to generate component
The generator creates starter files for all three parts of the component: @@ -462,7 +456,7 @@ The new product alert component takes a product as input from the product list.
- Product alert button added to products over $700 + Product alert button added to products over $700
@@ -553,7 +547,7 @@ The "Notify Me" button doesn't do anything yet. In this section, you'll set up t
- Product alert notification confirmation dialog + Product alert notification confirmation dialog
@@ -589,11 +583,11 @@ To continue exploring Angular, choose either of the following options: 要继续探索 Angular,请选择以下选项之一: -* [Continue to the "Routing" section](getting-started/routing "Getting Started: Routing") to create a product details page that can be accessed by clicking a product name and that has its own URL pattern. +* [Continue to the "Routing" section](start/routing "Getting Started: Routing") to create a product details page that can be accessed by clicking a product name and that has its own URL pattern. [继续浏览“路由”部分](getting-started/routing "入门:路由"),创建一个商品详情页面,通过单击商品名称,可以访问该页面,该页面有自己的 URL 模式。 -* [Skip ahead to the "Deployment" section](getting-started/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development. +* [Skip ahead to the "Deployment" section](start/deployment "Getting Started: Deployment") to move to local development, or deploy your app to Firebase or your own server. [跳到“部署”部分](getting-started/deployment "入门:部署"),把你的应用部署到 Firebase 或转成本地开发。 diff --git a/aio/content/getting-started/routing.md b/aio/content/start/routing.md similarity index 93% rename from aio/content/getting-started/routing.md rename to aio/content/start/routing.md index 13192ce53d..f8a8d1784a 100644 --- a/aio/content/getting-started/routing.md +++ b/aio/content/start/routing.md @@ -3,7 +3,7 @@ # 路由 -At the end of [Your First App](getting-started "Getting Started: Your First App"), the online store application has a basic product catalog. +At the end of [Your First App](start "Getting Started: Your First App"), the online store application has a basic product catalog. The app doesn't have any variable states or navigation. There is one URL, and that URL always displays the "My Store" page with a list of products and their descriptions. @@ -117,7 +117,7 @@ The app is already set up to use the Angular router and to use routing to naviga
- Product details page with updated URL + Product details page with updated URL
## Using route information @@ -194,7 +194,7 @@ Now, when the user clicks on a name in the product list, the router navigates yo
- Product details page with updated URL and full details displayed + Product details page with updated URL and full details displayed
@@ -230,11 +230,11 @@ To continue exploring Angular, choose either of the following options: 要继续探索 Angular,请选择以下选项之一: -* [Continue to the "Managing Data" section](getting-started/data "Getting Started: Managing Data") to add the shopping cart feature, using a service to manage the cart data and using HTTP to retrieve external data for shipping prices. +* [Continue to the "Managing Data" section](start/data "Getting Started: Managing Data") to add the shopping cart feature, using a service to manage the cart data and using HTTP to retrieve external data for shipping prices. [继续浏览“管理数据”部分](getting-started/data "入门:管理数据"),以添加购物车功能,使用服务来管理购物车数据,并通过 HTTP 检索配送价格的外部数据。 -* [Skip ahead to the Deployment section](getting-started/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development. +* [Skip ahead to the Deployment section](start/deployment "Getting Started: Deployment") to deploy your app to Firebase or move to local development. [跳到部署部分](getting-started/deployment "入门:部署"),把你的应用部署到 Firebase 或转成本地开发。 diff --git a/aio/content/tutorial/index.md b/aio/content/tutorial/index.md index 49965538d0..2dfc32968b 100644 --- a/aio/content/tutorial/index.md +++ b/aio/content/tutorial/index.md @@ -1,28 +1,30 @@ -

Tutorial: Tour of Heroes

+

Tour of Heroes App and Tutorial

教程:英雄指南

-This _Tour of Heroes_ tutorial provides a deep dive into the fundamentals of Angular. -It shows you how to set up your local development environment and develop an app using the [Angular CLI tool](cli "CLI command reference"). - -这个**英雄指南**教程深入讲解了 Angular 的基本知识。它向你展示了如何搭建本地开发环境,以及如何使用 [Angular CLI 工具](cli "CLI command reference")开发一个应用。 -
-
Getting Started - Stackblitz
+ +
Getting Started Tutorial
快速起步 - Stackblitz
-We recently introduced a [**new Getting Started**](getting-started) that leverages the [StackBlitz](https://stackblitz.com/) online development environment. -We recommend the new Getting Started for anyone who wants to quickly learn the essentials of Angular, in the context of building an online store app. -The new Getting Started covers the same major topics as this Tour of Heroes—components, template syntax, routing, services, and accessing data via HTTP—in a condensed format. +If you're new to Angular, see the [**Getting Started tutorial.**](start) +The Getting Started tutorial covers the same major topics as this Tour of Heroes—components, template syntax, routing, services, and accessing data via HTTP—in a condensed format, following the most current best practices. 我们最近引入了一个[**新的快速上手**](getting-started),它基于在线开发环境 [StackBlitz](https://stackblitz.com/)。 我们建议每个想要快速掌握 Angular 基础知识的人使用那个新的快速上手,来构建一个在线商店应用程序。 新的快速上手以更精简的格式涵盖了与本《英雄指南》相同的主题:组件、模板语法、路由、服务以及通过 HTTP 访问数据等。 +**This Tour of Heroes tutorial** is the conceptual basis for many examples in this documentation set. Reading this introduction page provides sufficient context for working with those examples. You do not need to do this tutorial to understand those other examples. The Tour of Heroes tutorial is maintained here for context and continuity. + +**这份《英雄指南》教程**是本文档中很多范例的基础。阅读此简介页面可以为那些例子提供充足的上下文。你不用实做这个教程就能理解其它范例。这份《英雄指南》教程只是为了保持一个上下文和连续性。 +
+This _Tour of Heroes_ tutorial provides an introduction to the fundamentals of Angular. +It shows you how to set up your local development environment and develop an app using the [Angular CLI tool](cli "CLI command reference"). + In this _Tour of Heroes_ tutorial, you will build an app that helps a staffing agency manage its stable of heroes. 在这个**英雄指南**教程中,你将构建一个应用,来帮助招聘机构管理一群英雄。 @@ -78,10 +80,20 @@ Angular can do whatever you need it to do. 你将学到足够的 Angular 知识,并确信 Angular 确实能提供你所需的支持。 +
+ +
Solution
+ +
最终解
+ After completing all tutorial steps, the final app will look like this: . 完成本教程的所有步骤之后,最终的应用会是这样的:。 +
+ + + ## What you'll build ## 你要构建出什么 diff --git a/aio/content/tutorial/toh-pt0.md b/aio/content/tutorial/toh-pt0.md index 92031f6b48..921ee4985e 100644 --- a/aio/content/tutorial/toh-pt0.md +++ b/aio/content/tutorial/toh-pt0.md @@ -29,27 +29,11 @@ In this part of the tutorial, you'll do the following: ## Set up your environment -## 设置开发环境 +## 搭建开发环境 -To set up your development environment, follow these instructions in [Getting Started](guide/quickstart): - -要想设置开发环境,请按照[快速上手](guide/quickstart) 中的说明进行操作: - -* [Prerequisites](guide/quickstart#prerequisites) - - [先决条件](guide/quickstart#prerequisites) - -* [Install the Angular CLI](guide/quickstart#install-cli) - - [安装 Angular CLI](guide/quickstart#install-cli) - -
- -**Note:** You do not need to complete the entire Getting Started. After you complete the above two sections of Getting Started, your environment is set up. Continue below to create the Tour of Heroes workspace and an initial app project. - -**注意:**你不用做完整个快速上手。只要完成了上面这两个部分,你的环境就已经设置好了。然后继续下面的步骤来创建一个《英雄指南》的工作区和一个初始应用项目。 -
+To set up your development environment, follow the instructions in [Local Environment Setup](guide/setup-local "Setting up for Local Development"). +要想搭建开发环境,请遵循[搭建本地环境](guide/setup-local "Setting up for Local Development")中的步骤进行操作。 ## Create a new workspace and an initial application diff --git a/aio/content/tutorial/toh-pt2.md b/aio/content/tutorial/toh-pt2.md index 3ba210541c..bf7ed3a7e1 100644 --- a/aio/content/tutorial/toh-pt2.md +++ b/aio/content/tutorial/toh-pt2.md @@ -286,14 +286,6 @@ Binding expressions in the template that refer to properties of `selectedHero` & 但模板中的绑定表达式引用了 `selectedHero` 的属性(表达式为 `{{selectedHero.name}}`),这必然会失败,因为你还没选过英雄呢。 -Now, click one of the list items. -The app seems to be working again. -The heroes appear in a list and details about the clicked hero appear at the bottom of the page. - -现在,点击列表中的一个条目。 -应用又能正常工作了。 -列表中又显示出了英雄们,并且选中的英雄的详情再次出现在了页面底部。 - #### The fix - hide empty details with _*ngIf_ #### 修复 —— 使用 _*ngIf_ 隐藏空白的详情 @@ -321,11 +313,16 @@ Don't forget the asterisk (*) in front of `ngIf`. It's a critical part of the sy After the browser refreshes, the list of names reappears. The details area is blank. -Click a hero and its details appear. +Click a hero in the list of heroes and its details appear. +The app seems to be working again. +The heroes appear in a list and details about the clicked hero appear at the bottom of the page. + 浏览器刷新之后,英雄名字的列表又出现了。 详情部分仍然是空。 -点击一个英雄,它的详情就出现了。 +从英雄列表中点击一个英雄,它的详情就出现了。 +应用又能工作了。 +英雄们出现在列表中,而被点击的英雄出现在了页面底部。 #### Why it works diff --git a/aio/content/tutorial/toh-pt5.md b/aio/content/tutorial/toh-pt5.md index 26e916137c..9687bd7e1d 100644 --- a/aio/content/tutorial/toh-pt5.md +++ b/aio/content/tutorial/toh-pt5.md @@ -769,9 +769,9 @@ You can click a hero in the dashboard or in the heroes list and navigate to that 你可以在仪表盘或英雄列表中点击一个英雄来导航到该英雄的详情视图。 If you paste `localhost:4200/detail/11` in the browser address bar, -the router navigates to the detail view for the hero with `id: 11`, "Mr. Nice". +the router navigates to the detail view for the hero with `id: 11`, "Dr Nice". -如果你在浏览器的地址栏中粘贴了 `localhost:4200/detail/11`,路由器也会导航到 `id: 11` 的英雄("Mr. Nice")的详情视图。 +如果你在浏览器的地址栏中粘贴了 `localhost:4200/detail/11`,路由器也会导航到 `id: 11` 的英雄("Dr. Nice")的详情视图。 {@a goback} diff --git a/aio/content/tutorial/toh-pt6.md b/aio/content/tutorial/toh-pt6.md index 390c579920..4cf58349cb 100644 --- a/aio/content/tutorial/toh-pt6.md +++ b/aio/content/tutorial/toh-pt6.md @@ -98,8 +98,24 @@ Install the *In-memory Web API* package from _npm_ npm install angular-in-memory-web-api --save -Import the `HttpClientInMemoryWebApiModule` and the `InMemoryDataService` class, -which you will create in a moment. + +The class `src/app/in-memory-data.service.ts` is generated by the following command: + + + ng generate service InMemoryData + + +This class has the following content: + + + +This file replaces `mock-heroes.ts`, which is now safe to delete. + +When your server is ready, detach the *In-memory Web API*, and the app's requests will go through to the server. + +Now back to the `HttpClient` story. + +Import the `HttpClientInMemoryWebApiModule` and the `InMemoryDataService` class. 导入 `HttpClientInMemoryWebApiModule` 和 `InMemoryDataService` 类(你很快就要创建它)。 @@ -126,32 +142,6 @@ that primes the in-memory database. `forRoot()` 配置方法接受一个 `InMemoryDataService` 类(初期的内存数据库)作为参数。 -The class `src/app/in-memory-data.service.ts` is generated by the following command: - -`src/app/in-memory-data.service.ts` 类是通过下列命令生成的: - - - ng generate service InMemoryData - - -This class has the following content: - -该类的内容如下: - - - -This file replaces `mock-heroes.ts`, which is now safe to delete. - -这个文件替代了 `mock-heroes.ts`(你可以安全删除它了)。 - -When your server is ready, detach the *In-memory Web API*, and the app's requests will go through to the server. - -等你真实的服务器就绪时,就可以删除这个*内存 Web API*,该应用的请求就会直接发给真实的服务器。 - -Now back to the `HttpClient` story. - -现在,回来看 `HttpClient`。 - {@a import-heroes} ## Heroes and HTTP diff --git a/aio/firebase.json b/aio/firebase.json index a506a10892..5f3b52588d 100644 --- a/aio/firebase.json +++ b/aio/firebase.json @@ -16,18 +16,21 @@ {"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"}, // Guide renames/removals - {"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/docs/*/latest/cli-quickstart.html", "destination": "/start"}, {"type": 301, "source": "/docs/*/latest/glossary.html", "destination": "/guide/glossary"}, - {"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/docs/*/latest/quickstart.html", "destination": "/start"}, {"type": 301, "source": "/docs/*/latest/guide/server-communication.html", "destination": "/guide/http"}, {"type": 301, "source": "/docs/*/latest/guide/style-guide.html", "destination": "/guide/styleguide"}, - {"type": 301, "source": "/guide/cli-quickstart", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/guide/cli-quickstart", "destination": "/start"}, {"type": 301, "source": "/guide/service-worker-getstart", "destination": "/guide/service-worker-getting-started"}, {"type": 301, "source": "/guide/service-worker-comm", "destination": "/guide/service-worker-communications"}, {"type": 301, "source": "/guide/service-worker-configref", "destination": "/guide/service-worker-config"}, {"type": 301, "source": "/guide/webpack", "destination": "https://v5.angular.io/guide/webpack"}, {"type": 301, "source": "/guide/setup-systemjs-anatomy", "destination": "/guide/file-structure"}, {"type": 301, "source": "/guide/change-log", "destination": "https://github.com/angular/angular/blob/master/CHANGELOG.md"}, + {"type": 301, "source": "/guide/quickstart", "destination": "/start"}, + {"type": 301, "source": "/getting-started", "destination": "/start"}, + {"type": 301, "source": "/getting-started/:rest*", "destination": "/start/:rest*"}, // some top level guide pages on old site were moved below the guide folder {"type": 301, "source": "/styleguide", "destination": "/guide/styleguide"}, @@ -108,7 +111,7 @@ {"type": 301, "source": "/docs/styleguide*", "destination": "/guide/styleguide"}, {"type": 301, "source": "/guide/metadata", "destination": "/guide/aot-compiler"}, {"type": 301, "source": "/guide/ngmodule", "destination": "/guide/ngmodules"}, - {"type": 301, "source": "/guide/learning-angular*", "destination": "/guide/quickstart"}, + {"type": 301, "source": "/guide/learning-angular*", "destination": "/start"}, {"type": 301, "source": "/testing", "destination": "/guide/testing"}, {"type": 301, "source": "/testing/**", "destination": "/guide/testing"}, diff --git a/aio/ngsw-config.json b/aio/ngsw-config.json index 269ad7e6d6..eefdd3d1a9 100644 --- a/aio/ngsw-config.json +++ b/aio/ngsw-config.json @@ -95,6 +95,9 @@ "!/docs/?*", "!/docs/*/**", "!/guide/change-log", + "!/getting-started", + "!/getting-started.html", + "!/getting-started/**", "!/guide/cli-quickstart", "!/guide/cli-quickstart.html", "!/guide/cli-quickstart/", @@ -121,6 +124,8 @@ "!/guide/webpack/", "!/guide/setup-systemjs-anatomy", "!/guide/setup-systemjs-anatomy.html", + "!/guide/quickstart", + "!/guide/quickstart.html", "!/news", "!/news.html", "!/news/", diff --git a/aio/package.json b/aio/package.json index b4ff9e137e..974aec7e34 100644 --- a/aio/package.json +++ b/aio/package.json @@ -20,7 +20,7 @@ "build-local": "yarn ~~build", "prebuild-with-ivy": "yarn setup-local && node scripts/switch-to-ivy", "build-with-ivy": "yarn ~~build", - "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 8dd9b98ac", + "extract-cli-command-docs": "node tools/transforms/cli-docs-package/extract-cli-commands.js 2bbd7f288", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint", "test": "yarn check-env && ng test", "pree2e": "yarn check-env && yarn update-webdriver", @@ -133,6 +133,7 @@ "html": "^1.0.0", "html-minifier": "^3.5.21", "http-server-spa": "^1.3.0", + "http-server": "^0.11.1", "ignore": "^3.3.3", "image-size": "^0.5.1", "jasmine": "^2.6.0", diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index 14cf8a8afb..13f6a2bc0d 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -2,8 +2,8 @@ "aio": { "master": { "uncompressed": { - "runtime-es5": 2980, - "runtime-es2015": 2986, + "runtime-es5": 2516, + "runtime-es2015": 2522, "main-es5": 504760, "main-es2015": 443497, "polyfills-es5": 128751, diff --git a/aio/src/app/custom-elements/code/code-example.component.spec.ts b/aio/src/app/custom-elements/code/code-example.component.spec.ts index ace165ed76..54431dad80 100644 --- a/aio/src/app/custom-elements/code/code-example.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-example.component.spec.ts @@ -96,5 +96,5 @@ class HostComponent { path = 'code-path'; hidecopy: boolean | string = false; - @ViewChild(CodeExampleComponent) codeExampleComponent: CodeExampleComponent; + @ViewChild(CodeExampleComponent, {static: true}) codeExampleComponent: CodeExampleComponent; } diff --git a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts index 1d9dc2b36d..a7be5e8f4c 100644 --- a/aio/src/app/custom-elements/code/code-tabs.component.spec.ts +++ b/aio/src/app/custom-elements/code/code-tabs.component.spec.ts @@ -92,5 +92,5 @@ describe('CodeTabsComponent', () => { ` }) class HostComponent { - @ViewChild(CodeTabsComponent) codeTabsComponent: CodeTabsComponent; + @ViewChild(CodeTabsComponent, {static: true}) codeTabsComponent: CodeTabsComponent; } diff --git a/aio/src/app/custom-elements/code/code.component.spec.ts b/aio/src/app/custom-elements/code/code.component.spec.ts index a814754ab7..320542fbe0 100644 --- a/aio/src/app/custom-elements/code/code.component.spec.ts +++ b/aio/src/app/custom-elements/code/code.component.spec.ts @@ -284,7 +284,7 @@ class HostComponent implements AfterViewInit { region: string; header: string; - @ViewChild(CodeComponent) codeComponent: CodeComponent; + @ViewChild(CodeComponent, {static: false}) codeComponent: CodeComponent; ngAfterViewInit() { this.setCode(oneLineCode); diff --git a/aio/src/app/custom-elements/custom-elements.module.ts b/aio/src/app/custom-elements/custom-elements.module.ts index f2000a391f..064dfb8e40 100644 --- a/aio/src/app/custom-elements/custom-elements.module.ts +++ b/aio/src/app/custom-elements/custom-elements.module.ts @@ -1,10 +1,10 @@ -import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core'; +import { NgModule } from '@angular/core'; import { ROUTES} from '@angular/router'; import { ElementsLoader } from './elements-loader'; import { - ELEMENT_MODULE_PATHS, - ELEMENT_MODULE_PATHS_AS_ROUTES, - ELEMENT_MODULE_PATHS_TOKEN + ELEMENT_MODULE_LOAD_CALLBACKS, + ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES, + ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN } from './element-registry'; import { LazyCustomElementComponent } from './lazy-custom-element.component'; @@ -13,13 +13,12 @@ import { LazyCustomElementComponent } from './lazy-custom-element.component'; exports: [ LazyCustomElementComponent ], providers: [ ElementsLoader, - { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }, - { provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: ELEMENT_MODULE_PATHS }, + { provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: ELEMENT_MODULE_LOAD_CALLBACKS }, // Providing these routes as a signal to the build system that these modules should be // registered as lazy-loadable. // TODO(andrewjs): Provide first-class support for providing this. - { provide: ROUTES, useValue: ELEMENT_MODULE_PATHS_AS_ROUTES, multi: true }, + { provide: ROUTES, useValue: ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES, multi: true }, ], }) export class CustomElementsModule { } diff --git a/aio/src/app/custom-elements/element-registry.ts b/aio/src/app/custom-elements/element-registry.ts index 0a80932e9a..b4a7cadd5a 100644 --- a/aio/src/app/custom-elements/element-registry.ts +++ b/aio/src/app/custom-elements/element-registry.ts @@ -1,44 +1,45 @@ import { InjectionToken, Type } from '@angular/core'; +import { LoadChildrenCallback } from '@angular/router'; // Modules containing custom elements must be set up as lazy-loaded routes (loadChildren) // TODO(andrewjs): This is a hack, Angular should have first-class support for preparing a module // that contains custom elements. -export const ELEMENT_MODULE_PATHS_AS_ROUTES = [ +export const ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES = [ { selector: 'aio-announcement-bar', - loadChildren: './announcement-bar/announcement-bar.module#AnnouncementBarModule' + loadChildren: () => import('./announcement-bar/announcement-bar.module').then(mod => mod.AnnouncementBarModule) }, { selector: 'aio-api-list', - loadChildren: './api/api-list.module#ApiListModule' + loadChildren: () => import('./api/api-list.module').then(mod => mod.ApiListModule) }, { selector: 'aio-contributor-list', - loadChildren: './contributor/contributor-list.module#ContributorListModule' + loadChildren: () => import('./contributor/contributor-list.module').then(mod => mod.ContributorListModule) }, { selector: 'aio-file-not-found-search', - loadChildren: './search/file-not-found-search.module#FileNotFoundSearchModule' + loadChildren: () => import('./search/file-not-found-search.module').then(mod => mod.FileNotFoundSearchModule) }, { selector: 'aio-resource-list', - loadChildren: './resource/resource-list.module#ResourceListModule' + loadChildren: () => import('./resource/resource-list.module').then(mod => mod.ResourceListModule) }, { selector: 'aio-toc', - loadChildren: './toc/toc.module#TocModule' + loadChildren: () => import('./toc/toc.module').then(mod => mod.TocModule) }, { selector: 'code-example', - loadChildren: './code/code-example.module#CodeExampleModule' + loadChildren: () => import('./code/code-example.module').then(mod => mod.CodeExampleModule) }, { selector: 'code-tabs', - loadChildren: './code/code-tabs.module#CodeTabsModule' + loadChildren: () => import('./code/code-tabs.module').then(mod => mod.CodeTabsModule) }, { selector: 'live-example', - loadChildren: './live-example/live-example.module#LiveExampleModule' + loadChildren: () => import('./live-example/live-example.module').then(mod => mod.LiveExampleModule) } ]; @@ -51,10 +52,10 @@ export interface WithCustomElementComponent { } /** Injection token to provide the element path modules. */ -export const ELEMENT_MODULE_PATHS_TOKEN = new InjectionToken('aio/elements-map'); +export const ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN = new InjectionToken>('aio/elements-map'); /** Map of possible custom element selectors to their lazy-loadable module paths. */ -export const ELEMENT_MODULE_PATHS = new Map(); -ELEMENT_MODULE_PATHS_AS_ROUTES.forEach(route => { - ELEMENT_MODULE_PATHS.set(route.selector, route.loadChildren); +export const ELEMENT_MODULE_LOAD_CALLBACKS = new Map(); +ELEMENT_MODULE_LOAD_CALLBACKS_AS_ROUTES.forEach(route => { + ELEMENT_MODULE_LOAD_CALLBACKS.set(route.selector, route.loadChildren); }); diff --git a/aio/src/app/custom-elements/elements-loader.spec.ts b/aio/src/app/custom-elements/elements-loader.spec.ts index acb2befd82..36185a2067 100644 --- a/aio/src/app/custom-elements/elements-loader.spec.ts +++ b/aio/src/app/custom-elements/elements-loader.spec.ts @@ -1,13 +1,14 @@ import { + Compiler, ComponentFactory, - ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleFactoryLoader, + ComponentFactoryResolver, ComponentRef, Injector, NgModuleFactory, NgModuleRef, - Type + Type, } from '@angular/core'; import { TestBed, fakeAsync, flushMicrotasks } from '@angular/core/testing'; import { ElementsLoader } from './elements-loader'; -import { ELEMENT_MODULE_PATHS_TOKEN, WithCustomElementComponent } from './element-registry'; +import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry'; interface Deferred { @@ -17,20 +18,25 @@ interface Deferred { describe('ElementsLoader', () => { let elementsLoader: ElementsLoader; + let compiler: Compiler; beforeEach(() => { const injector = TestBed.configureTestingModule({ providers: [ ElementsLoader, - { provide: NgModuleFactoryLoader, useClass: FakeModuleFactoryLoader }, - { provide: ELEMENT_MODULE_PATHS_TOKEN, useValue: new Map([ - ['element-a-selector', 'element-a-module-path'], - ['element-b-selector', 'element-b-module-path'] + { + provide: ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, useValue: new Map< + string, () => Promise | Type> + >([ + ['element-a-selector', () => Promise.resolve(new FakeModuleFactory('element-a-module'))], + ['element-b-selector', () => Promise.resolve(new FakeModuleFactory('element-b-module'))], + ['element-c-selector', () => Promise.resolve(FakeCustomElementModule)] ])}, ] }); elementsLoader = injector.get(ElementsLoader); + compiler = injector.get(Compiler); }); describe('loadContainedCustomElements()', () => { @@ -148,7 +154,7 @@ describe('ElementsLoader', () => { // Verify the right component was loaded/registered. const Ctor = definedSpy.calls.argsFor(0)[1]; - expect(Ctor.observedAttributes).toEqual(['element-a-module-path']); + expect(Ctor.observedAttributes).toEqual(['element-a-module']); })); it('should wait until the element is defined', fakeAsync(() => { @@ -222,6 +228,20 @@ describe('ElementsLoader', () => { expect(definedSpy).toHaveBeenCalledTimes(1); }) ); + + it('should be able to load and register an element after compiling its NgModule', fakeAsync(() => { + const compilerSpy = spyOn(compiler, 'compileModuleAsync') + .and.returnValue(Promise.resolve(new FakeModuleFactory('element-c-module'))); + + elementsLoader.loadCustomElement('element-c-selector'); + flushMicrotasks(); + + expect(definedSpy).toHaveBeenCalledTimes(1); + expect(definedSpy).toHaveBeenCalledWith('element-c-selector', jasmine.any(Function)); + + expect(compilerSpy).toHaveBeenCalledTimes(1); + expect(compilerSpy).toHaveBeenCalledWith(FakeCustomElementModule); + })); }); }); @@ -282,13 +302,6 @@ class FakeModuleFactory extends NgModuleFactory { } } -class FakeModuleFactoryLoader extends NgModuleFactoryLoader { - load(modulePath: string): Promise> { - const fakeModuleFactory = new FakeModuleFactory(modulePath); - return Promise.resolve(fakeModuleFactory); - } -} - function returnPromisesFromSpy(spy: jasmine.Spy): Deferred[] { const deferreds: Deferred[] = []; spy.and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject}))); diff --git a/aio/src/app/custom-elements/elements-loader.ts b/aio/src/app/custom-elements/elements-loader.ts index 67cbb4185c..f06894082b 100644 --- a/aio/src/app/custom-elements/elements-loader.ts +++ b/aio/src/app/custom-elements/elements-loader.ts @@ -1,19 +1,27 @@ -import { Inject, Injectable, NgModuleFactoryLoader, NgModuleRef } from '@angular/core'; -import { ELEMENT_MODULE_PATHS_TOKEN } from './element-registry'; +import { + Compiler, + Inject, + Injectable, + NgModuleFactory, + NgModuleRef, + Type, +} from '@angular/core'; +import { ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN, WithCustomElementComponent } from './element-registry'; import { from, Observable, of } from 'rxjs'; import { createCustomElement } from '@angular/elements'; +import { LoadChildrenCallback } from '@angular/router'; @Injectable() export class ElementsLoader { /** Map of unregistered custom elements and their respective module paths to load. */ - private elementsToLoad: Map; + private elementsToLoad: Map; /** Map of custom elements that are in the process of being loaded and registered. */ private elementsLoading = new Map>(); - constructor(private moduleFactoryLoader: NgModuleFactoryLoader, - private moduleRef: NgModuleRef, - @Inject(ELEMENT_MODULE_PATHS_TOKEN) elementModulePaths: Map) { + constructor(private moduleRef: NgModuleRef, + @Inject(ELEMENT_MODULE_LOAD_CALLBACKS_TOKEN) elementModulePaths: Map, + private compiler: Compiler) { this.elementsToLoad = new Map(elementModulePaths); } @@ -42,9 +50,23 @@ export class ElementsLoader { if (this.elementsToLoad.has(selector)) { // Load and register the custom element (for the first time). - const modulePath = this.elementsToLoad.get(selector)!; - const loadedAndRegistered = this.moduleFactoryLoader - .load(modulePath) + const modulePathLoader = this.elementsToLoad.get(selector)!; + const loadedAndRegistered = + (modulePathLoader() as Promise | Type>) + .then(elementModuleOrFactory => { + /** + * With View Engine, the NgModule factory is created and provided when loaded. + * With Ivy, only the NgModule class is provided loaded and must be compiled. + * This uses the same mechanism as the deprecated `SystemJsNgModuleLoader` in + * in `packages/core/src/linker/system_js_ng_module_factory_loader.ts` + * to pass on the NgModuleFactory, or compile the NgModule and return its NgModuleFactory. + */ + if (elementModuleOrFactory instanceof NgModuleFactory) { + return elementModuleOrFactory; + } else { + return this.compiler.compileModuleAsync(elementModuleOrFactory); + } + }) .then(elementModuleFactory => { const elementModuleRef = elementModuleFactory.create(this.moduleRef.injector); const injector = elementModuleRef.injector; diff --git a/aio/src/testing/doc-viewer-utils.ts b/aio/src/testing/doc-viewer-utils.ts index a078e492ec..6a3ca95137 100644 --- a/aio/src/testing/doc-viewer-utils.ts +++ b/aio/src/testing/doc-viewer-utils.ts @@ -37,7 +37,7 @@ export class TestDocViewerComponent extends DocViewerComponent { }) export class TestParentComponent { currentDoc?: DocumentContents|null; - @ViewChild(DocViewerComponent) docViewer: DocViewerComponent; + @ViewChild(DocViewerComponent, {static: true}) docViewer: DocViewerComponent; } // Mock services. diff --git a/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts b/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts index fe9e8025c8..7c954f9c0d 100644 --- a/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts +++ b/aio/tests/deployment/e2e/smoke-tests.e2e-spec.ts @@ -25,7 +25,7 @@ describe(browser.baseUrl, () => { describe('(marketing pages)', () => { const textPerUrl: { [key: string]: string } = { features: 'features & benefits', - docs: 'what is angular?', + docs: 'introduction to the angular docs', events: 'events', resources: 'explore angular resources', }; @@ -45,9 +45,9 @@ describe(browser.baseUrl, () => { api: 'api list', 'guide/architecture': 'architecture', 'guide/http': 'httpclient', - 'guide/quickstart': 'getting started', 'guide/security': 'security', tutorial: 'tutorial', + start: 'getting started', }; Object.keys(textPerUrl).forEach(url => { diff --git a/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt b/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt index 1f86c1ed97..2928dddbbb 100644 --- a/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt +++ b/aio/tests/deployment/shared/URLS_TO_REDIRECT.txt @@ -171,10 +171,16 @@ /docs/ts/latest/api/platform-browser-dynamic/index/workerAppDynamicPlatform-let.html /api/platform-browser-dynamic/workerAppDynamicPlatform /docs/ts/latest/api/testing/fakeAsync-function.html /api/core/testing/fakeAsync /docs/ts/latest/cookbook/ts-to-js.html https://v2.angular.io/docs/ts/latest/cookbook/ts-to-js.html -/guide/cli-quickstart /guide/quickstart -/guide/learning-angular /guide/quickstart -/guide/learning-angular.html /guide/quickstart +/getting-started /start +/getting-started/routing /start/routing +/getting-started/data /start/data +/getting-started/forms /start/forms +/getting-started/deployment /start/deployment +/guide/cli-quickstart /start +/guide/learning-angular /start +/guide/learning-angular.html /start /guide/metadata /guide/aot-compiler +/guide/quickstart /start /guide/service-worker-getstart /guide/service-worker-getting-started /guide/service-worker-comm /guide/service-worker-communications /guide/service-worker-configref /guide/service-worker-config diff --git a/aio/tests/e2e/src/app.e2e-spec.ts b/aio/tests/e2e/src/app.e2e-spec.ts index 48f68b904a..db162c5660 100644 --- a/aio/tests/e2e/src/app.e2e-spec.ts +++ b/aio/tests/e2e/src/app.e2e-spec.ts @@ -75,7 +75,7 @@ describe('site App', function() { it('should show the tutorial index page at `/tutorial` after jitterbugging through features', () => { // check that we can navigate directly to the tutorial page page.navigateTo('tutorial'); - expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i); + expect(page.getDocViewerText()).toMatch(/Tour of Heroes App and Tutorial/i); // navigate to a different page page.click(page.getTopMenuLink('features')); @@ -86,8 +86,8 @@ describe('site App', function() { // Tutorial folder should still be expanded because this test runs in wide mode // Navigate to the tutorial introduction via a link in the sidenav - page.click(page.getNavItem(/introduction/i)); - expect(page.getDocViewerText()).toMatch(/Tutorial: Tour of Heroes/i); + page.click(page.getNavItem(/The Hero Editor/i)); + expect(page.getDocViewerText()).toMatch(/The Hero Editor/i); }); it('should render `{@example}` dgeni tags as `` elements with HTML escaped content', () => { diff --git a/aio/tools/examples/README.md b/aio/tools/examples/README.md index 786216ec7f..22d712698c 100644 --- a/aio/tools/examples/README.md +++ b/aio/tools/examples/README.md @@ -17,21 +17,23 @@ As mentioned, many of the documentation pages contain snippets extracted from re To achieve that, all those applications needs to contain a basic boilerplate. E.g. a `node_modules` folder, `package.json` with scripts, etc. -No one wants to maintain the boilerplate on each example, so the goal of this tool is to provide a -set of boilerplates that works in all the examples. +No one wants to maintain the boilerplate on each example, so the goal of this tool is to provide a set of files that works across all the examples. ### Boilerplate files -Inside `/aio/tools/examples/shared/boilerplate` you will find a set of folders representing each -boilerplate. +Inside `/aio/tools/examples/shared/boilerplate` you will find a set of folders representing each project type. -Currently you will find the next boilerplates: +Currently you will find the next project types: - -* CLI - For CLI based examples. This is the default one, to be used in the majority of the examples. +* cli - For CLI based examples. This is the default one, to be used in the majority of the examples. +* getting-started - CLI-based with its own set of styles. +* i18n - CLI-based with additional scripts for internationalization. +* ivy - CLI-based with additional configuration for running the examples with the Ivy renderer and ngstc compiler. +* schematics - CLI-based with additional scripts for building schematics. +* service-worker - CLI-based with additional packages and configuration for service workers. * systemjs - Currently in deprecation, only used in a few examples. -* i18n - Based on the CLI one, features a few scripts for i18n. -* universal - Based on the cli with a extra server for universal. +* testing - CLI-based with additional styles for jasmine testing. +* universal - CLI-based with an extra server target. There is also a `common` folder that contains files used in all different examples. @@ -41,11 +43,14 @@ Each example is identified by an **example-config.json** configuration file in i This configuration file indicates what type of boilerplate this example needs. E.g. ```json -{ projectType: 'universal' } +{ + "projectType": "cli", + "useCommonBoilerplate": true +} ``` If the file is empty then the default type of cli is assumed. -When the boilerplate tooling runs, it will copy into the example folder all of the appropriate boilerplate files. +When the boilerplate tooling runs, it will copy into the example folder all of the appropriate files based on the project type. ### A node_modules to share @@ -89,4 +94,13 @@ that name. It also has an optional `--setup` flag to run the `example-boilerplate.js` script and install the latest `webdriver`. -It will create a `/aio/protractor-results-txt` file when it finishes running tests. +It will create a `/aio/protractor-results.txt` file when it finishes running tests. + +### Updating example dependencies + +With every major release, we update the examples to be on the latest version. The following steps to update are: + +* In the `shared/package.json` file, bump all the `@angular/*`, `@angular-devkit/*`, `rxjs`, `typescript`, and `zone.js` package versions to the version that corresponds with the [framework version](../../../package.json). +* In the `shared` folder, run `yarn` to update the dependencies for the shared `node_modules` and the `yarn.lock` file. +* In the `boilerplate` folder, go through each sub-folder and update the `package.json` dependencies if one is present. +* Follow the [update guide](./shared/boilerplate/UPDATING_CLI.md) to update the common files used in the examples based on project type. \ No newline at end of file diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 0bc5596607..d126e16370 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -13,9 +13,9 @@ const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples'); const BOILERPLATE_PATHS = { cli: [ 'src/environments/environment.prod.ts', 'src/environments/environment.ts', - 'src/assets/.gitkeep', 'src/browserslist', 'src/favicon.ico', 'src/karma.conf.js', - 'src/polyfills.ts', 'src/test.ts', 'src/tsconfig.app.json', 'src/tsconfig.spec.json', - 'src/tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor.conf.js', 'e2e/tsconfig.e2e.json', + 'src/assets/.gitkeep', 'browserslist', 'src/favicon.ico', 'karma.conf.js', + 'src/polyfills.ts', 'src/test.ts', 'tsconfig.app.json', 'tsconfig.spec.json', + 'tslint.json', 'e2e/src/app.po.ts', 'e2e/protractor.conf.js', 'e2e/tsconfig.json', '.editorconfig', 'angular.json', 'package.json', 'tsconfig.json', 'tslint.json' ], systemjs: [ @@ -29,13 +29,16 @@ const BOILERPLATE_PATHS = { // This maps the CLI files that exists in a parent folder const cliRelativePath = BOILERPLATE_PATHS.cli.map(file => `../cli/${file}`); -BOILERPLATE_PATHS.elements = [...cliRelativePath, 'tsconfig.json']; - BOILERPLATE_PATHS.i18n = [...cliRelativePath, 'angular.json', 'package.json']; BOILERPLATE_PATHS['service-worker'] = [...cliRelativePath, 'angular.json', 'package.json']; -BOILERPLATE_PATHS.testing = [...cliRelativePath, 'angular.json']; +BOILERPLATE_PATHS.testing = [ + ...cliRelativePath, + 'angular.json', + 'src/tsconfig.app.json', + 'src/tsconfig.spec.json' +]; BOILERPLATE_PATHS.universal = [...cliRelativePath, 'angular.json', 'package.json']; @@ -46,7 +49,7 @@ BOILERPLATE_PATHS['getting-started'] = [ BOILERPLATE_PATHS.ivy = { systemjs: ['rollup-config.js', 'tsconfig-aot.json'], - cli: ['src/tsconfig.app.json'] + cli: ['tsconfig.app.json'] }; BOILERPLATE_PATHS.schematics = [ @@ -72,9 +75,9 @@ class ExampleBoilerPlate { } if (ivy) { - // We only need the "fesm5" bundles as the CLI webpack build does not need + // We only need the "es2015" bundles as the CLI webpack build does not need // any other formats for building and serving. - shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc --properties module`); + shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc --properties es2015`); } exampleFolders.forEach(exampleFolder => { @@ -93,7 +96,7 @@ class ExampleBoilerPlate { // Copy the boilerplate common files const useCommonBoilerplate = exampleConfig.useCommonBoilerplate !== false; - + if (useCommonBoilerplate) { BOILERPLATE_PATHS.common.forEach(filePath => this.copyFile(BOILERPLATE_COMMON_BASE_PATH, exampleFolder, filePath)); } diff --git a/aio/tools/examples/run-example-e2e.js b/aio/tools/examples/run-example-e2e.js index e420b7752b..f0c1f6770d 100644 --- a/aio/tools/examples/run-example-e2e.js +++ b/aio/tools/examples/run-example-e2e.js @@ -26,11 +26,6 @@ const IGNORED_EXAMPLES = [ const fixmeIvyExamples = [ // fixmeIvy('unknown') app fails at runtime due to missing external service (goog is undefined) 'i18n', - // Needs a Angular CLI synced with the Ivy loadChildren: string support after - // https://github.com/angular/angular/pull/28685 - 'lazy-loading-ngmodules', - 'router', - 'ngmodules', ]; if (argv.ivy) { diff --git a/aio/tools/examples/shared/boilerplate/.gitignore b/aio/tools/examples/shared/boilerplate/.gitignore new file mode 100644 index 0000000000..ceed8f2aed --- /dev/null +++ b/aio/tools/examples/shared/boilerplate/.gitignore @@ -0,0 +1,2 @@ +**/yarn.lock +**/package-lock.json \ No newline at end of file diff --git a/aio/tools/examples/shared/boilerplate/UPDATING_CLI.md b/aio/tools/examples/shared/boilerplate/UPDATING_CLI.md index 8092025c30..3e0dee3883 100644 --- a/aio/tools/examples/shared/boilerplate/UPDATING_CLI.md +++ b/aio/tools/examples/shared/boilerplate/UPDATING_CLI.md @@ -1,56 +1,50 @@ -# How to update the CLI boilerplate +# How to update the CLI project -The boilerplate is updated by hand so you normally update it every minor version unless there is a major bug to fix. +The Angular CLI default setup is updated using `ng update`. Any necessary file changes will be done automatically through migration schematics. -## Getting a new boilerplate - -The first thing would be updating the CLI globally +In the `cli` folder, update the Angular CLI depedencies to the latest version: ``` -npm i -g @angular/cli +ng update @angular/cli --next ``` -Then create a new dummy project in a temporary folder outside angular +Then update the Angular Framework dependencies to the latest version: ``` -ng new dummy +ng update @angular/core --next ``` -Now you have a fresh application to get our new boilerplate files. +Commit any changes to the `cli` folder to the repository. -## Updating files +## Updating other CLI-based projects -From `dummy` you can replace the following files into `aio/tools/examples/shared/boilerplate/cli`: +Along with the boilerplate files for the `cli` folder, the other cli-based projects need to be updated also. Each cli-based project has slightly modified files specific to the project type. Make sure any necessary changes to these projects are made also to be in alignment with the `cli` project files. -* .editorconfig -* angular.json -* package.json -* tsconfig.json -* tslint.json -* e2e/src/app.po.ts -* e2e/protractor.conf.js -* e2e/tsconfig.e2e.json -* src/environments/environment.prod.ts -* src/environments/environment.ts -* src/browserslist -* src/favicon.ico -* src/karma.conf.js -* src/polyfills.js -* src/styles.css -* src/test.ts -* src/tsconfig.app.json -* src/tsconfig.spec.json -* src/tslint.json -* src/typings.d.ts +The specific changes to each project type are listed below: -### angular.json - -Update the `project > name` to `angular.io-example`. - -### package.json - -Update the `name` to `angular.io-example`. - -### src/tsconfig.app.json - -This file is small enough and there are a few new excludes, update by hand. +* i18n + - angular.json + - Includes additional configurations for `build`, `serve`, and `e2e` for different locales + - package.json + - Includes custom scripts for building and serving different locales +* ivy + - cli/tsconfig.app.json + - Includes an `angularCompilerOptions` object with `enableIvy` set to `true` +* schematics + - angular.json + - Includes a `my-lib` project that contains a library with example schematics +* service-worker + - angular.json + - Has `serviceWorker` set to `true` in the `production` build target + - package.json + - Includes `@angular/service-worker` in `dependencies` +* testing + - angular.json + - Includes `src/test.css` in the `styles` for the `test` target +* universal + - angular.json + - Includes a `server` target in the `build` architect runners + - package.json + - Includes custom scripts for building the `server` + - Includes additional `dependencies` on `@nguniversal/common`, `@nguniversal/express-engine`, and `@nguniversal/module-map-ngfactory-loader` + - Includes `devDependencies` on `@angular/platform-server`, and `ts-loader` diff --git a/aio/tools/examples/shared/boilerplate/cli/.editorconfig b/aio/tools/examples/shared/boilerplate/cli/.editorconfig index 6e87a003da..e89330a618 100644 --- a/aio/tools/examples/shared/boilerplate/cli/.editorconfig +++ b/aio/tools/examples/shared/boilerplate/cli/.editorconfig @@ -1,4 +1,4 @@ -# Editor configuration, see http://editorconfig.org +# Editor configuration, see https://editorconfig.org root = true [*] diff --git a/aio/tools/examples/shared/boilerplate/cli/.gitignore b/aio/tools/examples/shared/boilerplate/cli/.gitignore index ee5c9d8336..f4f46a5fee 100644 --- a/aio/tools/examples/shared/boilerplate/cli/.gitignore +++ b/aio/tools/examples/shared/boilerplate/cli/.gitignore @@ -4,10 +4,16 @@ /dist /tmp /out-tsc +# Only exists if Bazel was run +/bazel-out # dependencies /node_modules +# profiling files +chrome-profiler-events.json +speed-measure-plugin.json + # IDEs and editors /.idea .project @@ -23,6 +29,7 @@ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.history/* # misc /.sass-cache diff --git a/aio/tools/examples/shared/boilerplate/cli/angular.json b/aio/tools/examples/shared/boilerplate/cli/angular.json index 3735b82a1b..51a129b26c 100644 --- a/aio/tools/examples/shared/boilerplate/cli/angular.json +++ b/aio/tools/examples/shared/boilerplate/cli/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,7 +17,7 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" @@ -76,37 +76,31 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -118,15 +112,6 @@ "devServerTarget": "angular.io-example:serve:production" } } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } } } } diff --git a/aio/tools/examples/shared/boilerplate/cli/browserslist b/aio/tools/examples/shared/boilerplate/cli/browserslist new file mode 100644 index 0000000000..b23d6dc5bd --- /dev/null +++ b/aio/tools/examples/shared/boilerplate/cli/browserslist @@ -0,0 +1,15 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +# Googlebot uses an older version of Chrome +# For additional information see: https://developers.google.com/search/docs/guides/rendering + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/aio/tools/examples/shared/boilerplate/cli/e2e/protractor.conf.js b/aio/tools/examples/shared/boilerplate/cli/e2e/protractor.conf.js index 98b6d13e75..c8a6464753 100644 --- a/aio/tools/examples/shared/boilerplate/cli/e2e/protractor.conf.js +++ b/aio/tools/examples/shared/boilerplate/cli/e2e/protractor.conf.js @@ -1,15 +1,19 @@ +// @ts-check // Protractor configuration file, see link for more information // https://github.com/angular/protractor/blob/master/lib/config.ts const { SpecReporter } = require('jasmine-spec-reporter'); +/** + * @type { import("protractor").Config } + */ exports.config = { allScriptsTimeout: 11000, specs: [ './src/**/*.e2e-spec.ts' ], capabilities: { - 'browserName': 'chrome', + 'browserName': 'chrome' }, directConnect: true, baseUrl: 'http://localhost:4200/', @@ -21,7 +25,7 @@ exports.config = { }, onPrepare() { require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.e2e.json') + project: require('path').join(__dirname, './tsconfig.json') }); jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); } diff --git a/aio/tools/examples/shared/boilerplate/cli/e2e/src/app.po.ts b/aio/tools/examples/shared/boilerplate/cli/e2e/src/app.po.ts index 82ea75ba50..5776aa9eb8 100644 --- a/aio/tools/examples/shared/boilerplate/cli/e2e/src/app.po.ts +++ b/aio/tools/examples/shared/boilerplate/cli/e2e/src/app.po.ts @@ -2,10 +2,10 @@ import { browser, by, element } from 'protractor'; export class AppPage { navigateTo() { - return browser.get('/'); + return browser.get(browser.baseUrl) as Promise; } - getParagraphText() { - return element(by.css('app-root h1')).getText(); + getTitleText() { + return element(by.css('app-root h1')).getText() as Promise; } } diff --git a/aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.e2e.json b/aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.json similarity index 85% rename from aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.e2e.json rename to aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.json index 77d311e88d..39b800f789 100644 --- a/aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.e2e.json +++ b/aio/tools/examples/shared/boilerplate/cli/e2e/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "outDir": "../out-tsc/app", + "outDir": "../out-tsc/e2e", "module": "commonjs", "target": "es5", "types": [ diff --git a/aio/tools/examples/shared/boilerplate/cli/src/karma.conf.js b/aio/tools/examples/shared/boilerplate/cli/karma.conf.js similarity index 82% rename from aio/tools/examples/shared/boilerplate/cli/src/karma.conf.js rename to aio/tools/examples/shared/boilerplate/cli/karma.conf.js index 4a9730b9b6..f439e1ec78 100644 --- a/aio/tools/examples/shared/boilerplate/cli/src/karma.conf.js +++ b/aio/tools/examples/shared/boilerplate/cli/karma.conf.js @@ -16,8 +16,8 @@ module.exports = function (config) { clearContext: false // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../coverage'), - reports: ['html', 'lcovonly'], + dir: require('path').join(__dirname, './coverage/angular.io-example'), + reports: ['html', 'lcovonly', 'text-summary'], fixWebpackSourcePaths: true }, reporters: ['progress', 'kjhtml'], @@ -26,6 +26,7 @@ module.exports = function (config) { logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], - singleRun: false + singleRun: false, + restartOnFileChange: true }); }; diff --git a/aio/tools/examples/shared/boilerplate/cli/package.json b/aio/tools/examples/shared/boilerplate/cli/package.json index f1e44a1384..43774dbb37 100644 --- a/aio/tools/examples/shared/boilerplate/cli/package.json +++ b/aio/tools/examples/shared/boilerplate/cli/package.json @@ -12,40 +12,41 @@ }, "private": true, "dependencies": { - "@angular/animations": "^7.0.0", - "@angular/common": "^7.0.0", - "@angular/compiler": "^7.0.0", - "@angular/core": "^7.0.0", - "@angular/forms": "^7.0.0", - "@angular/platform-browser": "^7.0.0", - "@angular/platform-browser-dynamic": "^7.0.0", - "@angular/router": "^7.0.0", + "@angular/animations": "^8.0.0", + "@angular/common": "^8.0.0", + "@angular/compiler": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/forms": "^8.0.0", + "@angular/platform-browser": "^8.0.0", + "@angular/platform-browser-dynamic": "^8.0.0", + "@angular/router": "^8.0.0", "angular-in-memory-web-api": "^0.8.0", "core-js": "^2.5.4", "rxjs": "^6.5.1", + "tslib": "^1.9.0", "web-animations-js": "^2.3.1", "zone.js": "~0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.10.0", - "@angular/cli": "^7.0.0", - "@angular/compiler-cli": "^7.0.0", - "@angular/language-service": "^7.0.0", - "@types/jasmine": "~2.8.8", + "@angular-devkit/build-angular": "^0.800.0", + "@angular/cli": "^8.0.0", + "@angular/compiler-cli": "^8.0.0", + "@angular/language-service": "^8.0.0", + "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", - "codelyzer": "~4.3.0", + "codelyzer": "~5.0.0", "jasmine-core": "~2.99.1", - "jasmine-marbles": "^0.4.0", + "jasmine-marbles": "^0.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~3.0.0", + "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-node": "~7.0.0", - "tslint": "~5.11.0", - "typescript": "~3.1.1" + "tslint": "~5.15.0", + "typescript": "~3.4.4" } } diff --git a/aio/tools/examples/shared/boilerplate/cli/src/browserslist b/aio/tools/examples/shared/boilerplate/cli/src/browserslist deleted file mode 100644 index c6cb1d38d7..0000000000 --- a/aio/tools/examples/shared/boilerplate/cli/src/browserslist +++ /dev/null @@ -1,11 +0,0 @@ -# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries -# -# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed - -> 0.5% -last 2 versions -Firefox ESR -not dead -not IE 9-11 diff --git a/aio/tools/examples/shared/boilerplate/cli/src/polyfills.ts b/aio/tools/examples/shared/boilerplate/cli/src/polyfills.ts index 5efc9c9c5e..246bab0c51 100644 --- a/aio/tools/examples/shared/boilerplate/cli/src/polyfills.ts +++ b/aio/tools/examples/shared/boilerplate/cli/src/polyfills.ts @@ -18,29 +18,6 @@ * BROWSER POLYFILLS */ -/** IE9, IE10 and IE11 requires all of the following polyfills. **/ -// import 'core-js/es6/symbol'; -// import 'core-js/es6/object'; -// import 'core-js/es6/function'; -// import 'core-js/es6/parse-int'; -// import 'core-js/es6/parse-float'; -// import 'core-js/es6/number'; -// import 'core-js/es6/math'; -// import 'core-js/es6/string'; -// import 'core-js/es6/date'; -// import 'core-js/es6/array'; -// import 'core-js/es6/regexp'; -// import 'core-js/es6/map'; -// import 'core-js/es6/weak-map'; -// import 'core-js/es6/set'; - -/** - * If the application will be indexed by Google Search, the following is required. - * Googlebot uses a renderer based on Chrome 41. - * https://developers.google.com/search/docs/guides/rendering - **/ -// import 'core-js/es6/array'; - /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. @@ -58,31 +35,39 @@ import 'core-js/es7/reflect'; * Web Animations `@angular/platform-browser/animations` * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - **/ + */ // import 'web-animations-js'; // Run `npm install --save web-animations-js`. /** * By default, zone.js will patch all possible macroTask and DomEvents * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * */ - // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - - /* - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - */ -// (window as any).__Zone_enable_cross_context_check = true; - /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ import 'zone.js/dist/zone'; // Included with Angular CLI. - /*************************************************************************************************** * APPLICATION IMPORTS */ diff --git a/aio/tools/examples/shared/boilerplate/cli/src/tslint.json b/aio/tools/examples/shared/boilerplate/cli/src/tslint.json deleted file mode 100644 index 52e2c1a5a7..0000000000 --- a/aio/tools/examples/shared/boilerplate/cli/src/tslint.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../tslint.json", - "rules": { - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ] - } -} diff --git a/aio/tools/examples/shared/boilerplate/cli/tsconfig.app.json b/aio/tools/examples/shared/boilerplate/cli/tsconfig.app.json new file mode 100644 index 0000000000..f0259083da --- /dev/null +++ b/aio/tools/examples/shared/boilerplate/cli/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/test.ts", + "src/**/*.spec.ts", + "src/**/*.avoid.ts", + "src/**/*.0.ts", + "src/**/*.1.ts", + "src/**/*.1b.ts", + "src/**/*.2.ts", + "src/**/*.3.ts", + "src/**/*.4.ts", + "src/**/*.5.ts", + "src/**/*.6.ts", + "src/**/*.7.ts" + ] +} diff --git a/aio/tools/examples/shared/boilerplate/cli/tsconfig.json b/aio/tools/examples/shared/boilerplate/cli/tsconfig.json index 46aeded1b2..6ec9ceb174 100644 --- a/aio/tools/examples/shared/boilerplate/cli/tsconfig.json +++ b/aio/tools/examples/shared/boilerplate/cli/tsconfig.json @@ -5,11 +5,12 @@ "outDir": "./dist/out-tsc", "sourceMap": true, "declaration": false, - "module": "es2015", + "module": "esnext", "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es5", + "importHelpers": true, + "target": "es2015", "typeRoots": [ "node_modules/@types" ], diff --git a/aio/tools/examples/shared/boilerplate/cli/tsconfig.spec.json b/aio/tools/examples/shared/boilerplate/cli/tsconfig.spec.json new file mode 100644 index 0000000000..6400fde7d5 --- /dev/null +++ b/aio/tools/examples/shared/boilerplate/cli/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/aio/tools/examples/shared/boilerplate/cli/tslint.json b/aio/tools/examples/shared/boilerplate/cli/tslint.json index 2f3c13be4a..188bd78d32 100644 --- a/aio/tools/examples/shared/boilerplate/cli/tslint.json +++ b/aio/tools/examples/shared/boilerplate/cli/tslint.json @@ -1,32 +1,32 @@ { - "rulesDirectory": [ - "node_modules/codelyzer" - ], + "extends": "tslint:recommended", "rules": { - "arrow-return-shorthand": true, - "callable-types": true, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": true, + "array-type": false, + "arrow-parens": false, "deprecation": { "severity": "warn" }, - "eofline": true, - "forin": true, + "component-class-suffix": true, + "contextual-lifecycle": true, + "directive-class-suffix": true, + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ], "import-blacklist": [ true, "rxjs/Rx" ], - "import-spacing": true, - "indent": [ - true, - "spaces" - ], - "interface-over-type-literal": true, - "label-position": true, + "interface-name": false, + "max-classes-per-file": false, "max-line-length": [ true, 140 @@ -43,8 +43,7 @@ ] } ], - "no-arg": true, - "no-bitwise": true, + "no-consecutive-blank-lines": false, "no-console": [ true, "debug", @@ -53,78 +52,41 @@ "timeEnd", "trace" ], - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, "no-empty": false, - "no-empty-interface": true, - "no-eval": true, "no-inferrable-types": [ true, "ignore-params" ], - "no-misused-new": true, "no-non-null-assertion": true, "no-redundant-jsdoc": true, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-string-throw": true, "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unnecessary-initializer": true, - "no-unused-expression": true, - "no-var-keyword": true, - "object-literal-sort-keys": false, - "one-line": [ + "no-use-before-declare": true, + "no-var-requires": false, + "object-literal-key-quotes": [ true, - "check-open-brace", - "check-catch", - "check-else", - "check-whitespace" + "as-needed" ], - "prefer-const": true, + "object-literal-sort-keys": false, + "ordered-imports": false, "quotemark": [ true, "single" ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "unified-signatures": true, - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ], - "no-output-on-prefix": true, - "use-input-property-decorator": true, - "use-output-property-decorator": true, - "use-host-property-decorator": true, + "trailing-comma": false, + "no-conflicting-lifecycle": true, + "no-host-metadata-property": true, "no-input-rename": true, + "no-inputs-metadata-property": true, + "no-output-native": true, + "no-output-on-prefix": true, "no-output-rename": true, - "use-life-cycle-interface": true, - "use-pipe-transform-interface": true, - "component-class-suffix": true, - "directive-class-suffix": true - } -} + "no-outputs-metadata-property": true, + "template-banana-in-box": true, + "template-no-negated-async": true, + "use-lifecycle-interface": true, + "use-pipe-transform-interface": true + }, + "rulesDirectory": [ + "codelyzer" + ] +} \ No newline at end of file diff --git a/aio/tools/examples/shared/boilerplate/common/src/styles.css b/aio/tools/examples/shared/boilerplate/common/src/styles.css index aa756e7d70..d4e07e4175 100644 --- a/aio/tools/examples/shared/boilerplate/common/src/styles.css +++ b/aio/tools/examples/shared/boilerplate/common/src/styles.css @@ -13,7 +13,7 @@ body { margin: 2em; } body, input[text], button { - color: #888; + color: #333; font-family: Cambria, Georgia; } a { diff --git a/aio/tools/examples/shared/boilerplate/elements/tsconfig.json b/aio/tools/examples/shared/boilerplate/elements/tsconfig.json deleted file mode 100644 index 1a4f29fa89..0000000000 --- a/aio/tools/examples/shared/boilerplate/elements/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es2015", // Custom Elements require ES2015 classes (or polyfill). - "skipLibCheck": true, - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ] - } -} diff --git a/aio/tools/examples/shared/boilerplate/i18n/angular.json b/aio/tools/examples/shared/boilerplate/i18n/angular.json index e1dd5c6641..696875fcb7 100644 --- a/aio/tools/examples/shared/boilerplate/i18n/angular.json +++ b/aio/tools/examples/shared/boilerplate/i18n/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,7 +17,7 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" @@ -43,7 +43,14 @@ "aot": true, "extractLicenses": true, "vendorChunk": false, - "buildOptimizer": true + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] }, "production-fr": { "fileReplacements": [ @@ -102,37 +109,31 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -144,15 +145,6 @@ "devServerTarget": "angular.io-example:serve:production" } } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } } } } diff --git a/aio/tools/examples/shared/boilerplate/i18n/package.json b/aio/tools/examples/shared/boilerplate/i18n/package.json index 118df056e3..0dfad8564b 100644 --- a/aio/tools/examples/shared/boilerplate/i18n/package.json +++ b/aio/tools/examples/shared/boilerplate/i18n/package.json @@ -15,14 +15,14 @@ }, "private": true, "dependencies": { - "@angular/animations": "^7.0.0", - "@angular/common": "^7.0.0", - "@angular/compiler": "^7.0.0", - "@angular/core": "^7.0.0", - "@angular/forms": "^7.0.0", - "@angular/platform-browser": "^7.0.0", - "@angular/platform-browser-dynamic": "^7.0.0", - "@angular/router": "^7.0.0", + "@angular/animations": "^8.0.0", + "@angular/common": "^8.0.0", + "@angular/compiler": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/forms": "^8.0.0", + "@angular/platform-browser": "^8.0.0", + "@angular/platform-browser-dynamic": "^8.0.0", + "@angular/router": "^8.0.0", "angular-in-memory-web-api": "^0.8.0", "core-js": "^2.5.4", "rxjs": "^6.5.1", @@ -30,25 +30,25 @@ "zone.js": "~0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.10.0", - "@angular/cli": "^7.0.0", - "@angular/compiler-cli": "^7.0.0", - "@angular/language-service": "^7.0.0", - "@types/jasmine": "~2.8.8", + "@angular-devkit/build-angular": "^0.800.0", + "@angular/cli": "^8.0.0", + "@angular/compiler-cli": "^8.0.0", + "@angular/language-service": "^8.0.0", + "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", - "codelyzer": "~4.3.0", + "codelyzer": "~5.0.0", "jasmine-core": "~2.99.1", - "jasmine-marbles": "^0.4.0", + "jasmine-marbles": "^0.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~3.0.0", + "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-node": "~7.0.0", - "tslint": "~5.11.0", - "typescript": "~3.1.1" + "tslint": "~5.15.0", + "typescript": "~3.4.4" } } diff --git a/aio/tools/examples/shared/boilerplate/ivy/cli/src/tsconfig.app.json b/aio/tools/examples/shared/boilerplate/ivy/cli/src/tsconfig.app.json deleted file mode 100644 index d4d4aad743..0000000000 --- a/aio/tools/examples/shared/boilerplate/ivy/cli/src/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "types": [] - }, - "exclude": [ - "test.ts", - "**/*.spec.ts", - "**/*.avoid.ts", - "**/*.0.ts", - "**/*.1.ts", - "**/*.1b.ts", - "**/*.2.ts", - "**/*.3.ts", - "**/*.4.ts", - "**/*.5.ts", - "**/*.6.ts", - "**/*.7.ts" - ], - "angularCompilerOptions": { - "enableIvy": "ngtsc", - "allowEmptyCodegenFiles": true - } -} diff --git a/aio/tools/examples/shared/boilerplate/ivy/cli/tsconfig.app.json b/aio/tools/examples/shared/boilerplate/ivy/cli/tsconfig.app.json new file mode 100644 index 0000000000..43709e42fe --- /dev/null +++ b/aio/tools/examples/shared/boilerplate/ivy/cli/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/test.ts", + "src/**/*.spec.ts", + "src/**/*.avoid.ts", + "src/**/*.0.ts", + "src/**/*.1.ts", + "src/**/*.1b.ts", + "src/**/*.2.ts", + "src/**/*.3.ts", + "src/**/*.4.ts", + "src/**/*.5.ts", + "src/**/*.6.ts", + "src/**/*.7.ts" + ], + "angularCompilerOptions": { + "enableIvy": true + } +} diff --git a/aio/tools/examples/shared/boilerplate/ivy/systemjs/tsconfig-aot.json b/aio/tools/examples/shared/boilerplate/ivy/systemjs/tsconfig-aot.json index b113b6cade..d68fa9f6ec 100644 --- a/aio/tools/examples/shared/boilerplate/ivy/systemjs/tsconfig-aot.json +++ b/aio/tools/examples/shared/boilerplate/ivy/systemjs/tsconfig-aot.json @@ -7,13 +7,17 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": [ - "es2015", + "es2018", "dom" ], "removeComments": false, "noImplicitAny": true, "skipLibCheck": true, - "suppressImplicitAnyIndexErrors": true + "suppressImplicitAnyIndexErrors": true, + "importHelpers": true, + "typeRoots": [ + "node_modules/@types" + ] }, "files": [ "app/app.module.ts", @@ -21,6 +25,6 @@ ], "angularCompilerOptions": { "skipMetadataEmit": true, - "enableIvy": "ngtsc" + "enableIvy": true } } diff --git a/aio/tools/examples/shared/boilerplate/schematics/angular.json b/aio/tools/examples/shared/boilerplate/schematics/angular.json index 7923d1b35f..8975dbd47e 100644 --- a/aio/tools/examples/shared/boilerplate/schematics/angular.json +++ b/aio/tools/examples/shared/boilerplate/schematics/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,7 +17,7 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" @@ -76,37 +76,31 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -118,15 +112,6 @@ "devServerTarget": "angular.io-example:serve:production" } } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } } } }, diff --git a/aio/tools/examples/shared/boilerplate/service-worker/angular.json b/aio/tools/examples/shared/boilerplate/service-worker/angular.json index ba4dc645a4..5dba12fc05 100644 --- a/aio/tools/examples/shared/boilerplate/service-worker/angular.json +++ b/aio/tools/examples/shared/boilerplate/service-worker/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,7 +17,7 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" @@ -77,37 +77,31 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -119,15 +113,6 @@ "devServerTarget": "angular.io-example:serve:production" } } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } } } } diff --git a/aio/tools/examples/shared/boilerplate/service-worker/package.json b/aio/tools/examples/shared/boilerplate/service-worker/package.json index 4080f13b61..28317ef6e3 100644 --- a/aio/tools/examples/shared/boilerplate/service-worker/package.json +++ b/aio/tools/examples/shared/boilerplate/service-worker/package.json @@ -12,15 +12,15 @@ }, "private": true, "dependencies": { - "@angular/animations": "^7.0.0", - "@angular/common": "^7.0.0", - "@angular/compiler": "^7.0.0", - "@angular/core": "^7.0.0", - "@angular/forms": "^7.0.0", - "@angular/platform-browser": "^7.0.0", - "@angular/platform-browser-dynamic": "^7.0.0", - "@angular/router": "^7.0.0", - "@angular/service-worker": "^7.0.0", + "@angular/animations": "^8.0.0", + "@angular/common": "^8.0.0", + "@angular/compiler": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/forms": "^8.0.0", + "@angular/platform-browser": "^8.0.0", + "@angular/platform-browser-dynamic": "^8.0.0", + "@angular/router": "^8.0.0", + "@angular/service-worker": "^8.0.0", "angular-in-memory-web-api": "^0.8.0", "core-js": "^2.5.4", "rxjs": "^6.5.1", @@ -28,25 +28,25 @@ "zone.js": "~0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.10.0", - "@angular/cli": "^7.0.0", - "@angular/compiler-cli": "^7.0.0", - "@angular/language-service": "^7.0.0", - "@types/jasmine": "~2.8.8", + "@angular-devkit/build-angular": "^0.800.0", + "@angular/cli": "^8.0.0", + "@angular/compiler-cli": "^8.0.0", + "@angular/language-service": "^8.0.0", + "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", - "codelyzer": "~4.3.0", + "codelyzer": "~5.0.0", "jasmine-core": "~2.99.1", - "jasmine-marbles": "^0.4.0", + "jasmine-marbles": "^0.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~3.0.0", + "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-node": "~7.0.0", - "tslint": "~5.11.0", - "typescript": "~3.1.1" + "tslint": "~5.15.0", + "typescript": "~3.4.4" } } diff --git a/aio/tools/examples/shared/boilerplate/testing/angular.json b/aio/tools/examples/shared/boilerplate/testing/angular.json index 6d8dda4e1e..d04996cbb7 100644 --- a/aio/tools/examples/shared/boilerplate/testing/angular.json +++ b/aio/tools/examples/shared/boilerplate/testing/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,14 +17,13 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ - "src/styles.css", - "src/test.css" + "src/styles.css" ], "scripts": [] }, @@ -44,7 +43,14 @@ "aot": true, "extractLicenses": true, "vendorChunk": false, - "buildOptimizer": true + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] } } }, @@ -70,37 +76,32 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css", + "src/test.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { + }, "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -112,15 +113,6 @@ "devServerTarget": "angular.io-example:serve:production" } } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } } } } diff --git a/aio/tools/examples/shared/boilerplate/cli/src/tsconfig.app.json b/aio/tools/examples/shared/boilerplate/testing/src/tsconfig.app.json similarity index 89% rename from aio/tools/examples/shared/boilerplate/cli/src/tsconfig.app.json rename to aio/tools/examples/shared/boilerplate/testing/src/tsconfig.app.json index dbbf994fa4..ad0bfc37fd 100644 --- a/aio/tools/examples/shared/boilerplate/cli/src/tsconfig.app.json +++ b/aio/tools/examples/shared/boilerplate/testing/src/tsconfig.app.json @@ -16,6 +16,7 @@ "**/*.4.ts", "**/*.5.ts", "**/*.6.ts", - "**/*.7.ts" + "**/*.7.ts", + "**/testing" ] -} \ No newline at end of file +} diff --git a/aio/tools/examples/shared/boilerplate/cli/src/tsconfig.spec.json b/aio/tools/examples/shared/boilerplate/testing/src/tsconfig.spec.json similarity index 87% rename from aio/tools/examples/shared/boilerplate/cli/src/tsconfig.spec.json rename to aio/tools/examples/shared/boilerplate/testing/src/tsconfig.spec.json index de7733630e..ca77f8ff5c 100644 --- a/aio/tools/examples/shared/boilerplate/cli/src/tsconfig.spec.json +++ b/aio/tools/examples/shared/boilerplate/testing/src/tsconfig.spec.json @@ -13,6 +13,7 @@ ], "include": [ "**/*.spec.ts", - "**/*.d.ts" + "**/*.d.ts", + "**/testing" ] } diff --git a/aio/tools/examples/shared/boilerplate/universal/angular.json b/aio/tools/examples/shared/boilerplate/universal/angular.json index d8a77a38e4..babe0fcedb 100644 --- a/aio/tools/examples/shared/boilerplate/universal/angular.json +++ b/aio/tools/examples/shared/boilerplate/universal/angular.json @@ -4,11 +4,11 @@ "newProjectRoot": "projects", "projects": { "angular.io-example": { + "projectType": "application", + "schematics": {}, "root": "", "sourceRoot": "src", - "projectType": "application", "prefix": "app", - "schematics": {}, "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", @@ -17,7 +17,7 @@ "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", + "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" @@ -43,7 +43,14 @@ "aot": true, "extractLicenses": true, "vendorChunk": false, - "buildOptimizer": true + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] } } }, @@ -69,45 +76,31 @@ "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" - ] + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" ], "exclude": [ "**/node_modules/**" ] } }, - "server": { - "builder": "@angular-devkit/build-angular:server", - "options": { - "outputPath": "dist/server", - "main": "src/main.server.ts", - "tsConfig": "src/tsconfig.server.json" - } - } - } - }, - "angular.io-example-e2e": { - "root": "e2e/", - "projectType": "application", - "prefix": "", - "architect": { "e2e": { "builder": "@angular-devkit/build-angular:protractor", "options": { @@ -120,13 +113,12 @@ } } }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", + "server": { + "builder": "@angular-devkit/build-angular:server", "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] + "outputPath": "dist/server", + "main": "src/main.server.ts", + "tsConfig": "tsconfig.server.json" } } } diff --git a/aio/tools/examples/shared/boilerplate/universal/package.json b/aio/tools/examples/shared/boilerplate/universal/package.json index e94fc915d6..d8525c3064 100644 --- a/aio/tools/examples/shared/boilerplate/universal/package.json +++ b/aio/tools/examples/shared/boilerplate/universal/package.json @@ -16,18 +16,17 @@ }, "private": true, "dependencies": { - "@angular/animations": "^7.0.0", - "@angular/common": "^7.0.0", - "@angular/compiler": "^7.0.0", - "@angular/core": "^7.0.0", - "@angular/forms": "^7.0.0", - "@angular/http": "^7.0.0", - "@angular/platform-browser": "^7.0.0", - "@angular/platform-browser-dynamic": "^7.0.0", - "@angular/router": "^7.0.0", - "@nguniversal/common": "^7.0.0", - "@nguniversal/express-engine": "^7.0.0", - "@nguniversal/module-map-ngfactory-loader": "^7.0.0", + "@angular/animations": "^8.0.0", + "@angular/common": "^8.0.0", + "@angular/compiler": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/forms": "^8.0.0", + "@angular/platform-browser": "^8.0.0", + "@angular/platform-browser-dynamic": "^8.0.0", + "@angular/router": "^8.0.0", + "@nguniversal/common": "^8.0.0-rc.1", + "@nguniversal/express-engine": "^8.0.0-rc.1", + "@nguniversal/module-map-ngfactory-loader": "^8.0.0-rc.1", "angular-in-memory-web-api": "^0.8.0", "core-js": "^2.5.4", "rxjs": "^6.5.1", @@ -35,28 +34,28 @@ "zone.js": "~0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.10.0", - "@angular/cli": "^7.0.0", - "@angular/compiler-cli": "^7.0.0", - "@angular/language-service": "^7.0.0", - "@angular/platform-server": "^7.0.0", - "@types/jasmine": "~2.8.8", + "@angular-devkit/build-angular": "^0.800.0", + "@angular/cli": "^8.0.0", + "@angular/compiler-cli": "^8.0.0", + "@angular/language-service": "^8.0.0", + "@angular/platform-server": "^8.0.0", + "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", - "codelyzer": "~4.3.0", + "codelyzer": "~5.0.0", "jasmine-core": "~2.99.1", - "jasmine-marbles": "^0.4.0", + "jasmine-marbles": "^0.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~3.0.0", + "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-loader": "^4.2.0", "ts-node": "~7.0.0", "tslint": "~5.11.0", - "typescript": "~3.1.1", + "typescript": "~3.4.3", "webpack-cli": "^3.1.0" } } diff --git a/aio/tools/examples/shared/package.json b/aio/tools/examples/shared/package.json index 198677f158..e71f7b2528 100644 --- a/aio/tools/examples/shared/package.json +++ b/aio/tools/examples/shared/package.json @@ -18,35 +18,35 @@ "author": "", "license": "MIT", "dependencies": { - "@angular/animations": "^7.1.0", - "@angular/common": "^7.1.0", - "@angular/compiler": "^7.1.0", - "@angular/core": "^7.1.0", - "@angular/elements": "^7.1.0", - "@angular/forms": "^7.1.0", - "@angular/http": "^7.1.0", - "@angular/platform-browser": "^7.1.0", - "@angular/platform-browser-dynamic": "^7.1.0", - "@angular/router": "^7.1.0", - "@angular/service-worker": "^7.1.0", - "@angular/upgrade": "^7.1.0", - "@nguniversal/common": "^7.1.0", - "@nguniversal/express-engine": "^7.1.0", - "@nguniversal/module-map-ngfactory-loader": "^7.1.0", + "@angular/animations": "^8.0.0", + "@angular/common": "^8.0.0", + "@angular/compiler": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/elements": "^8.0.0", + "@angular/forms": "^8.0.0", + "@angular/platform-browser": "^8.0.0", + "@angular/platform-browser-dynamic": "^8.0.0", + "@angular/router": "^8.0.0", + "@angular/service-worker": "^8.0.0", + "@angular/upgrade": "^8.0.0", + "@nguniversal/common": "^8.0.0-rc.1", + "@nguniversal/express-engine": "^8.0.0-rc.1", + "@nguniversal/module-map-ngfactory-loader": "^8.0.0-rc.1", "angular-in-memory-web-api": "github:brandonroberts/in-memory-web-api-bazel#50a34d8", "core-js": "^2.5.4", "express": "^4.14.1", "rxjs": "^6.5.1", "systemjs": "0.19.39", "web-animations-js": "^2.3.1", + "tslib": "^1.9.0", "zone.js": "~0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "^0.11.0", - "@angular/cli": "^7.1.0", - "@angular/compiler-cli": "^7.1.0", - "@angular/language-service": "^7.1.0", - "@angular/platform-server": "^7.1.0", + "@angular-devkit/build-angular": "0.800.0", + "@angular/cli": "^8.0.0", + "@angular/compiler-cli": "^8.0.0", + "@angular/language-service": "^8.0.0", + "@angular/platform-server": "^8.0.0", "@types/angular": "^1.6.47", "@types/angular-animate": "^1.5.10", "@types/angular-mocks": "^1.6.0", @@ -59,14 +59,14 @@ "@types/node": "~8.9.4", "canonical-path": "1.0.0", "concurrently": "^3.0.0", - "http-server": "^0.9.0", - "jasmine-core": "~2.99.1", - "jasmine-marbles": "^0.4.0", + "http-server": "^0.11.1", + "jasmine-core": "~3.4.0", + "jasmine-marbles": "^0.5.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~3.0.0", + "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~1.1.2", + "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^0.2.2", "lite-server": "^2.2.2", "lodash": "^4.16.2", @@ -79,8 +79,8 @@ "source-map-explorer": "^1.3.2", "ts-loader": "^4.2.0", "ts-node": "~7.0.0", - "tslint": "~5.11.0", - "typescript": "~3.1.1", + "tslint": "~5.15.0", + "typescript": "~3.4.3", "webpack-cli": "^3.1.0" }, "repository": {} diff --git a/aio/tools/examples/shared/yarn.lock b/aio/tools/examples/shared/yarn.lock index 3540e04352..ff015fb6de 100644 --- a/aio/tools/examples/shared/yarn.lock +++ b/aio/tools/examples/shared/yarn.lock @@ -2,165 +2,152 @@ # yarn lockfile v1 -"@angular-devkit/architect@0.11.4": - version "0.11.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.11.4.tgz#f0cc3b4f1dd0128f6b41d3bb760bcf4c324cd063" - integrity sha512-2zi6S9tPlk52vyqN67IvFoeNgd0uxtrPlwl3TdvJ3wrH7sYGJnkQ+EzAE7cKUGWAV989BbNtx2YxhRDHnN21Fg== +"@angular-devkit/architect@0.800.0": + version "0.800.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.800.0.tgz#2f2ce1178b12b0c0fde455d00def60104b9cd08e" + integrity sha512-haXTS9EDaJfKyYiFylK2hObJH5DVGhX3OEn3OC7XUVlfKV3GRmC6NKXPjnxcN0wWP5zIV1c4xuB2N64mcxSnaA== dependencies: - "@angular-devkit/core" "7.1.4" - rxjs "6.3.3" + "@angular-devkit/core" "8.0.0" + rxjs "6.4.0" -"@angular-devkit/architect@0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.13.1.tgz#39597ce94f72d89bdd89ee567cb937cff4c13b98" - integrity sha512-QDmIbqde75ZZSEFbw6Q6kQWq4cY6C7D67yujXw6XTyubDNAs1tyXJyxTIB8vjSlEKwRizTTDd/B0ZXVcke3Mvw== +"@angular-devkit/build-angular@0.800.0": + version "0.800.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.800.0.tgz#6be1a0268daeadc7f07a73f1a1ec2759e4a0965b" + integrity sha512-JO9oT1VMhjuXF+OwLMBTMdRrS0jC2zIThO9UZYpE8oTtkL+17ra+5SN/fFg+2L7rx4kLUWDzcZGaVp9yTptXxw== dependencies: - "@angular-devkit/core" "7.3.1" - rxjs "6.3.3" - -"@angular-devkit/build-angular@^0.11.0": - version "0.11.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.11.4.tgz#795084e29c66a71da15227cf2ac29794aa807c7c" - integrity sha512-5WQAQB4heDqAotqjU3Tl8Ons0S/e16dKwVkQFdqfKPyBgmu4CyUH35eTV+i6i7un1Elg65U5GnA4MiUtApqVyw== - dependencies: - "@angular-devkit/architect" "0.11.4" - "@angular-devkit/build-optimizer" "0.11.4" - "@angular-devkit/build-webpack" "0.11.4" - "@angular-devkit/core" "7.1.4" - "@ngtools/webpack" "7.1.4" - ajv "6.5.3" - autoprefixer "9.3.1" + "@angular-devkit/architect" "0.800.0" + "@angular-devkit/build-optimizer" "0.800.0" + "@angular-devkit/build-webpack" "0.800.0" + "@angular-devkit/core" "8.0.0" + "@ngtools/webpack" "8.0.0" + ajv "6.10.0" + autoprefixer "9.5.1" + browserslist "4.5.5" + caniuse-api "3.0.0" circular-dependency-plugin "5.0.2" clean-css "4.2.1" - copy-webpack-plugin "4.5.4" - file-loader "2.0.0" + copy-webpack-plugin "5.0.2" + core-js "3.0.1" + file-loader "3.0.1" glob "7.1.3" - istanbul "0.4.5" istanbul-instrumenter-loader "3.0.1" - karma-source-map-support "1.3.0" - less "3.8.1" + karma-source-map-support "1.4.0" + less "3.9.0" less-loader "4.1.0" - license-webpack-plugin "2.0.2" - loader-utils "1.1.0" - mini-css-extract-plugin "0.4.4" + license-webpack-plugin "2.1.1" + loader-utils "1.2.3" + mini-css-extract-plugin "0.6.0" minimatch "3.0.4" - opn "5.3.0" + open "6.2.0" parse5 "4.0.0" - portfinder "1.0.17" - postcss "7.0.5" - postcss-import "12.0.0" + postcss "7.0.14" + postcss-import "12.0.1" postcss-loader "3.0.0" - raw-loader "0.5.1" - rxjs "6.3.3" + raw-loader "1.0.0" + rxjs "6.4.0" + sass "1.19.0" sass-loader "7.1.0" - semver "5.5.1" + semver "6.0.0" source-map-loader "0.2.4" - source-map-support "0.5.9" - speed-measure-webpack-plugin "1.2.3" + source-map-support "0.5.12" + speed-measure-webpack-plugin "1.3.1" stats-webpack-plugin "0.7.0" style-loader "0.23.1" stylus "0.54.5" stylus-loader "3.0.2" - terser-webpack-plugin "1.1.0" - tree-kill "1.2.0" - webpack "4.23.1" - webpack-dev-middleware "3.4.0" - webpack-dev-server "3.1.10" - webpack-merge "4.1.4" + terser-webpack-plugin "1.2.3" + tree-kill "1.2.1" + webpack "4.30.0" + webpack-dev-middleware "3.6.2" + webpack-dev-server "3.3.1" + webpack-merge "4.2.1" webpack-sources "1.3.0" webpack-subresource-integrity "1.1.0-rc.6" - optionalDependencies: - node-sass "4.10.0" + worker-plugin "3.1.0" -"@angular-devkit/build-optimizer@0.11.4": - version "0.11.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.11.4.tgz#d96b0e16a76f3825f173220a2de5f376fc5abaee" - integrity sha512-tAAWWFCcl918Q1JivlLvLFer8Qm4/THWbEneMwk5fQvG6/NgJLoa3itP/MCUq4qL6YHmp2DWkdWnWfRQCgHeFA== +"@angular-devkit/build-optimizer@0.800.0": + version "0.800.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.800.0.tgz#c2c8d5966801e243bf25d47b704b5b4de7cbdb3f" + integrity sha512-Cex/BynswHw+pvhiAwHlW51PFerGoa6J+wR93Te/4yIiCrX6KzWy6/v0RO5eY+iuebAr5QZDezcofB0Eg5yjiA== dependencies: - loader-utils "1.1.0" + loader-utils "1.2.3" source-map "0.5.6" - typescript "3.1.6" - webpack-sources "1.2.0" + typescript "3.4.4" + webpack-sources "1.3.0" -"@angular-devkit/build-webpack@0.11.4": - version "0.11.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.11.4.tgz#1397b21b6187eab0641830ece4c3b9faba00855e" - integrity sha512-4nEDXSbv3oDu27Rw5s2DMKmcOZYVAt76bryVF2SycSkDq3eAIiqmgw3G3CJJ4LTulXzDpaIpk02MvgbYkX+hvw== +"@angular-devkit/build-webpack@0.800.0": + version "0.800.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.800.0.tgz#304702b5cfcd6f4e4a578e9b9daccd8d9085eb2a" + integrity sha512-7lPbO1u6MMqU9G7kKWiELdFDqQGLuuiK6muqmO+nct8o1m5XpJ33HHfkxpF+nrt1qjKE1ZVCerAu0fNeaTyK0g== dependencies: - "@angular-devkit/architect" "0.11.4" - "@angular-devkit/core" "7.1.4" - rxjs "6.3.3" + "@angular-devkit/architect" "0.800.0" + "@angular-devkit/core" "8.0.0" + rxjs "6.4.0" + webpack-merge "4.2.1" -"@angular-devkit/core@7.1.4": - version "7.1.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-7.1.4.tgz#4d903fd2ecc259b716ae76da19695d03993e583c" - integrity sha512-3cBVHjSQjMyE/mIyOX82ekdybNRQlN+kUfmdZS6oVW9aV48vdxcVbEGdl8t1H4enMf89u8kXiAAET9jFaqWopg== +"@angular-devkit/core@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-8.0.0.tgz#a0ca65d8d0f928db9288316b1f3346d21f722213" + integrity sha512-wYf4zzpYj5Y673DG8iteK0GsDDuXBKN/TOXm4lUwmXcz8QHTD+BfR6qA5TBDqlMGpU7CP1/0vgbv2px17CDETQ== dependencies: - ajv "6.5.3" - chokidar "2.0.4" + ajv "6.10.0" fast-json-stable-stringify "2.0.0" - rxjs "6.3.3" + magic-string "0.25.2" + rxjs "6.4.0" source-map "0.7.3" -"@angular-devkit/core@7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-7.3.1.tgz#d92f6545796579cabdcfc29579a2c977f7a96c6c" - integrity sha512-56XDWWfIzOAkEk69lBLgmCYybPUA4yjunhmMlCk7vVdb7gbQUyzNjFD04Uj0GjlejatAQ5F76tRwygD9C+3RXQ== +"@angular-devkit/schematics@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-8.0.0.tgz#53d14646c6286b0397417990fc83e3e9a6ecf233" + integrity sha512-IXJOs/DkDqNbfG76sNNY5ePZ37rjkMUopmtvhN6/U1hQFwTpGa9N0bCHFphcKraXeS6Jfox5XwFEStc/1xyhfw== dependencies: - ajv "6.7.0" - chokidar "2.0.4" - fast-json-stable-stringify "2.0.0" - rxjs "6.3.3" - source-map "0.7.3" + "@angular-devkit/core" "8.0.0" + rxjs "6.4.0" -"@angular-devkit/schematics@7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-7.3.1.tgz#7dc704005b966ea6c1ee62f380120183bb76eee6" - integrity sha512-cd7usiasfSgw75INz72/VssrLr9tiVRYfo1TEdvr9ww0GuQbuQpB33xbV8W135eAV8+wzQ3Ce8ohaDHibvj6Yg== - dependencies: - "@angular-devkit/core" "7.3.1" - rxjs "6.3.3" - -"@angular/animations@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.2.4.tgz#4d0a0b9f14d6bfc38ca773613b61729d020435e6" - integrity sha512-Wx6cqU6koFOASlyl4aCygtbtROoehU6OKwV2EZTkfzHx6Eu/QyTiSa5kyoApVM5LMmCNeb8SxJMSAnKXztNl0A== +"@angular/animations@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-8.0.0.tgz#6286094babdb3879f7aefcd73aa31772469e50b4" + integrity sha512-hggSRi83rmocLwzrKZtmFcqPdivKSJqp2yiYaiNmJ2yQWJ1JW/Lurypv9H347RWxmwCCwC2kV8embTGbOXIFDQ== dependencies: tslib "^1.9.0" -"@angular/cli@^7.1.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-7.3.1.tgz#a18acdec84deb03a1fae79cae415bbc8f9c87ffa" - integrity sha512-8EvXYRhTqTaTk5PKv7VZxIWJiyG51R9RC9gtpRFx4bbnurqBHdEUxGMmaRsGT8QDbfvVsWnuakE0eeW1CrfZAQ== +"@angular/cli@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-8.0.0.tgz#0f65f60e8714b7b99f9425b862221e818959b0ac" + integrity sha512-F7zdAazejA94WKXULLWs0cj76/LkGx2Jb+yGE7QWx3jkp5j18KI3jTU+h9UOtxk0zgV4oSy9AQpJQVZFPZPQFA== dependencies: - "@angular-devkit/architect" "0.13.1" - "@angular-devkit/core" "7.3.1" - "@angular-devkit/schematics" "7.3.1" - "@schematics/angular" "7.3.1" - "@schematics/update" "0.13.1" + "@angular-devkit/architect" "0.800.0" + "@angular-devkit/core" "8.0.0" + "@angular-devkit/schematics" "8.0.0" + "@schematics/angular" "8.0.0" + "@schematics/update" "0.800.0" "@yarnpkg/lockfile" "1.1.0" + debug "^4.1.1" ini "1.3.5" - inquirer "6.2.1" + inquirer "6.3.1" npm-package-arg "6.1.0" - opn "5.4.0" - pacote "9.4.0" - semver "5.6.0" + open "6.2.0" + pacote "9.5.0" + read-package-tree "5.2.2" + semver "6.0.0" symbol-observable "1.2.0" + universal-analytics "^0.4.20" + uuid "^3.3.2" -"@angular/common@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.2.4.tgz#9f1ed530e5dc7613a263e015c203ead390d50336" - integrity sha512-3/i8RtnLTx/90gJHk5maE8zwsSiHgHvLItaa0qVfNlWiU0eCId/PL6TgDkut5vN9SQYL0oxhxFaVd35HmwsmuQ== +"@angular/common@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-8.0.0.tgz#700aeda9be8af96692fce0ea6bf6157f7c874c0e" + integrity sha512-iOAJZ0+1zTRHnHE/5G30+4Q66W1pfZkSkxZIXvgijZ+wtuNloYdWNy/IdZ/m7ayBI7A6FsYEhyMUoWz2HVEJNw== dependencies: tslib "^1.9.0" -"@angular/compiler-cli@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.2.4.tgz#3de23fd5f558a859a444c58dab18f2981c9c2937" - integrity sha512-UhLosSeuwFIfaGqGcYOh9WSOuzEpeuhIRAOt81MeqOQEqkoreUjfxrQq8XWNkdqsPZHtiptF5ZwXlMBxlj9jJg== +"@angular/compiler-cli@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-8.0.0.tgz#b53ebb5accc34a68bf7a63d16130ca7c568f8a51" + integrity sha512-Z0U0Ih8A7V3J1gq7AXnXbrGAD2ERmz7JbREJJRHDWiUNxIqGQiV3Odo1V8FL5n/cKvLwSYM2Ubvk10gb0+3njA== dependencies: canonical-path "1.0.0" - chokidar "^1.4.2" + chokidar "^2.1.1" convert-source-map "^1.5.1" dependency-graph "^0.7.2" magic-string "^0.25.0" @@ -169,89 +156,82 @@ shelljs "^0.8.1" source-map "^0.6.1" tslib "^1.9.0" - yargs "9.0.1" + yargs "13.1.0" -"@angular/compiler@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.2.4.tgz#133eb97fc3169ec9ff84f134eb9e3497fa37537e" - integrity sha512-+zyMzPCL45ePEV9nrnYJvhAVgp2Y19bDaq0f0YdZAqAjgDqHzXGGR6wX8GueyJWmUYWx5vwK6Apla4HwDrYA1w== +"@angular/compiler@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-8.0.0.tgz#302c987737e1473db3a113ff70fbbb315aa41b58" + integrity sha512-4rKsVFMNykF83tPL1VE1+j9kZ3cWHUsLOAB/VqmF64EcR/GsbjKog2v23rSso5kqUtPiVq/FWGYllW6qMdxtJA== dependencies: tslib "^1.9.0" -"@angular/core@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.2.4.tgz#a6c84940c8edcfa37158f666a1f99c6e4a97bf95" - integrity sha512-kfAxhIxl89PmB7y81FR/RAv0yWRFcEYxEnTwV+o8jKGfemAXtQ0g/Vh+lJR0SD/TBgFilMxotN1mhwH4A8GShw== +"@angular/core@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-8.0.0.tgz#bf7a582b818e9181d830219907470e2b865ba32f" + integrity sha512-mrkP1PTzqCmZGLYll+TDyawLXHzi+FcRPqSuRxCmDMthUUE93SLXT2yISDkx9aMPtFKgFr6KfrIkKuCz16BP/g== dependencies: tslib "^1.9.0" -"@angular/elements@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-7.2.4.tgz#4b77cb0f815deb3fd83d8a3c9a807bcca11c2373" - integrity sha512-5V4kFmpncQTJEVdREaSBb4DVjZz88eLQBjzzvEUM3r7szhbQ1DYuubG7Sj2a1iIYSI/HTkPPvX57rQk3vq6AEw== +"@angular/elements@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-8.0.0.tgz#7d25f95ba6854adb7361f106701cb1280394867f" + integrity sha512-vDs/O/9RNpmXaGJjmtFZ6MNFr2rqfB2qgq8tDcYkY7bAF11ulfwI2OXsI2vFnf4/qBlstdwXTmI5zar+ZU6MGA== dependencies: tslib "^1.9.0" -"@angular/forms@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.2.4.tgz#be89cf83ad16fa3c813c12e4cff85da5409cf7a0" - integrity sha512-DAtOrdlTRsgvmZrsvczCAkY8dhTwZb5DXBmPuSXh0UR9lvEiCgNHGbwEiIiIkAHpw1wSeXZrq0qyy/oJRvf18g== +"@angular/forms@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-8.0.0.tgz#6d636c4f83004290e1a5732a05e87148aaf6ed64" + integrity sha512-T6XdG3mALWzvnrN3fA1hAmfwvraiF1SPMWNXgPk2riuMf8CFdoro+tQZ4eo1islHrTTw5QzmqN8JJALfhAG6bg== dependencies: tslib "^1.9.0" -"@angular/http@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.2.4.tgz#fc151ac15c8c7542cb7242a430ad2319655c2ff5" - integrity sha512-kazJREm7MtSCYbE+9zU/CcUXI5Csu53PooeQlAp80/TOHqry6fVKIMHCI892Db9ScY2ds0SzbyTmrxEQo7PP1A== +"@angular/language-service@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-8.0.0.tgz#1ee4ce5003897cad53597da28f4c94fe30519bfb" + integrity sha512-vGk14oWroEo6ycO4cooznx57nn2sASmCQ/sdE8UVwySUKl940TsVzijgaGqapTepFof9sMqN77y2G15eRKQeAQ== + +"@angular/platform-browser-dynamic@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.0.0.tgz#c15f394579ff44f3752033de58edc1afa5065d59" + integrity sha512-dx7W7JoSFbsveexjZ/BPlsXbMDLWVLmRCo7IqLvibMrTbdpaaOCNJIXJk1X+f7JJrQ7SwlZaVkoLCMoDWw6fmA== dependencies: tslib "^1.9.0" -"@angular/language-service@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-7.2.4.tgz#db72460040b070410cbff678410c142f4d682af8" - integrity sha512-A9Rud/27hHMSUUjpgn57nVeLsoYgdvFwJhtlZA/oCuSpmlD+LqqBsEpPhivwn++u44+DSrFXsic29jlFnsBotw== - -"@angular/platform-browser-dynamic@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.2.4.tgz#24dce1bb0d9dab541b3b1b3eda3084a732f11b64" - integrity sha512-J/xWlmaYOPUoCHZ5TiIRiyYa4uRMtCz3aGdBfY8k/NWtNo8SCYaS3aut7Sk4RS5rK8aAVi+aYFlY5YOrlW+Hbg== +"@angular/platform-browser@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-8.0.0.tgz#fc7c55a0483e67e5606e499c129fda60ae8d4363" + integrity sha512-fTD+pTMbq+On9Uv3VXiei2lfuX7GX31dngm/Y4yWTFeW6eXy0+7kkfflzpLOb0hykCZvcXzarqCuEBBYNLrrOg== dependencies: tslib "^1.9.0" -"@angular/platform-browser@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.2.4.tgz#2cf5305878d0620d6b8c02eff00ac3ca8dbc5970" - integrity sha512-Klt8aKR5SP9bqfMfpSY5vQOY7AQEs8JGuZOk5Bfc2dUtYT2IEIvK2IqO8v2rcFRVO13HOPUxl328efyHqLgI7g== +"@angular/platform-server@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-8.0.0.tgz#87e80acba6b09955046dc0a9da7cd6b2e005061a" + integrity sha512-pA6m1okOfyy2qH5A6jUxrhx6z7eAG+ne7IM+j/6JUBDjp4KO9BC84aa/xfpZq5dsskl8E8II9c4hUKocMyeRjA== dependencies: - tslib "^1.9.0" - -"@angular/platform-server@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-7.2.4.tgz#276803cc7cac8da54d1ad2f05e052e4f8af64a85" - integrity sha512-3KbLHw7xMbkxun93HeYX8pSiPmFWim3ftvKbfPlB01fjhdZvhHpf39Dn4T7iyT1vrZMccXL87psv4/lJkTf04A== - dependencies: - domino "^2.1.0" + domino "^2.1.2" tslib "^1.9.0" xhr2 "^0.1.4" -"@angular/router@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.2.4.tgz#83f1997c2a4e6acda93b991b8d7f3dad2b3f91f0" - integrity sha512-T8Uqf2H1SV1MQI38WwYJ4aa+4NNnvlp2Tp/rkfg6tKcp/cLkKqE6OOfiy9lmW+i/624v8tMgYoBMOUNBjAG23g== +"@angular/router@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-8.0.0.tgz#26094fd473e17441b0ae8af4883ec1b4ea3ad569" + integrity sha512-DGUTb8qpndE5m716xh00GxuC8o7qamlqbUruGB+SQD6ynU7s5yLGxtKffxqb1BT63+YewpsVxc2Koruvb1qjDw== dependencies: tslib "^1.9.0" -"@angular/service-worker@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-7.2.4.tgz#d16d6d08c0d5c29c93e9f80cddc4013c9b8859fb" - integrity sha512-IYsHshkgCYYmWLwtP7wwk8tfwphE4IJrkUitEu+ST6x+er/K9LyLo09WQeEZHIwDaPm9icoqc3TJJdXI46mrmg== +"@angular/service-worker@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-8.0.0.tgz#5cb55bc39b29e318f5d72c1223a966c088f18568" + integrity sha512-MrR6Xb0X0+qO+bbdGasg7+xEvH7rPhgLpDn9FF/RwMMzj4qQmeJw9uRSJe9xMR6n0gNS7+cH8RptivOanY1QjQ== dependencies: tslib "^1.9.0" -"@angular/upgrade@^7.1.0": - version "7.2.4" - resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-7.2.4.tgz#49ecce8f8a6c290599179d5d96d19812a496145a" - integrity sha512-sRDXl2Uy9fZrMROfe7eNZDWwb1fgoWYzJ8VrviUCRakTJf3ZQLuEf4ToTc3KY/KGxEubA4jiuI8LXbyTTmRMQw== +"@angular/upgrade@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-8.0.0.tgz#7a756815c553350ba4cd64a0275ac6fc14286898" + integrity sha512-HRL99P13Mql3/kyrX/XUHqA8fdXC6QAmEnbcatAyCq6BGjJ047z1EFv/A6LHXXN5GFAgziyQbEqw3CnhjUeRVw== dependencies: tslib "^1.9.0" @@ -333,53 +313,52 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@ngtools/webpack@7.1.4": - version "7.1.4" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-7.1.4.tgz#632ece6ed8e05fe743554cc935be36a653376f01" - integrity sha512-8A15TPJzg3g7yI70QvBzJ253P32WAgCVre9nMaDdd22UmlbvN8Ke4RuQY7vYVTECLL+bWpFJEFXL+ThzCRUgeA== +"@ngtools/webpack@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-8.0.0.tgz#1a842f14b80f5430358374d0150659cb029a0198" + integrity sha512-IbljboxtAde95VbxpmCLzVMA9SrPPCFsBsDu2bmCjRGRSjnMVbXNPFm3fel4AxjT+St650w8RLJ4h6Fb+AV/Aw== dependencies: - "@angular-devkit/core" "7.1.4" + "@angular-devkit/core" "8.0.0" enhanced-resolve "4.1.0" - rxjs "6.3.3" - tree-kill "1.2.0" - webpack-sources "1.2.0" + rxjs "6.4.0" + tree-kill "1.2.1" + webpack-sources "1.3.0" -"@nguniversal/common@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@nguniversal/common/-/common-7.1.0.tgz#bd53e16a1dfce040413a6e3a3931fbc7ad2c8bb3" - integrity sha512-uEIJPzPk3u0MeOh2UhyDMsFlNemm1hHhvLaT5AJBBenoWcGkPRFuMilkZw7O12qStdthEZedfZYkOft2Fftpzg== +"@nguniversal/common@^8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@nguniversal/common/-/common-8.0.0-rc.1.tgz#21072437251765864d74add678b3a3540c4611aa" + integrity sha512-J/oNhuWFroB1JAWrXgRHFCdbYQ/XTWJxyUOi7pUaWfdF+kudjKFEVc2TARBmPSGEizHxkTIkysVHT817/L3lXQ== -"@nguniversal/express-engine@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@nguniversal/express-engine/-/express-engine-7.1.0.tgz#d48b1a821036382d62e4d4b122b5e44790c41c3f" - integrity sha512-otOA3WTjb+XnEDiyhwkvP0hE1gC7PiJrDoDFs5Q77SQ0ZAjuGzAIIgpQrPE8B+v0zmVj2oucDNCSZlmYWb1P/Q== +"@nguniversal/express-engine@^8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@nguniversal/express-engine/-/express-engine-8.0.0-rc.1.tgz#95f05b6fdac036f0deda7813e8ee08fa4b4edfd2" + integrity sha512-WGJZTxkCMgDHK3UQHi6h7AL9Us7Vroz+pAS60lKJ0oNUCxoJ9S+i4jXfb6rtR5DtOTBqUy8O8fQ0U0HZOhhoWA== -"@nguniversal/module-map-ngfactory-loader@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-7.1.0.tgz#70ea905c1b32c2edc484cb77aa7a3f3208069966" - integrity sha512-GYfb24OLJKBY58CgUsIsGgci5ceZAt4+GrVKh7RZRIHtZ/bjdGsvpIbfE9udqsnSowxIxHA5KzYHbC1x6AAB0A== +"@nguniversal/module-map-ngfactory-loader@^8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-8.0.0-rc.1.tgz#ca82a170fe72057b2379a55147580803c951319e" + integrity sha512-dPac8uahg4XHSvrXP0/XkU/LaFhAHJ8N9h93ttXfrEXNMukarOmbyKzAuX9DVjcE6+lll1UCZtsEweRvQBZPbw== -"@schematics/angular@7.3.1": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-7.3.1.tgz#6fcd7004210fa9305310c3109c084df5c5521776" - integrity sha512-0Ne8APPlTAjKg5CSZqluwCuW/5yPjr3ALCWzqwPxN0suE745usThtasBmqrjw0RMIt8nRqRgtg54Z7lCPO9ZFg== +"@schematics/angular@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-8.0.0.tgz#47954888fb8acbc3600235db7a46229c47fe5d9c" + integrity sha512-c/cFpe+u7Xh4xX3/kn9BSRY4YhdO0OsDbRK0pGLDJFFs5JGvwoURtNXn4/4dVlsj3PWyNhxK0Ljl3dyw3NQBHA== dependencies: - "@angular-devkit/core" "7.3.1" - "@angular-devkit/schematics" "7.3.1" - typescript "3.2.4" + "@angular-devkit/core" "8.0.0" + "@angular-devkit/schematics" "8.0.0" -"@schematics/update@0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.13.1.tgz#481475aee18b4a9472a06512b2e4d6429af68231" - integrity sha512-EHOqolT/d/jRGuVTCUESLpk8JNpuaPlsVHfeK7Kdp/t0wSEnmtOelZX4+leS25lGXDaDUF3138ntjrZR4n6bGw== +"@schematics/update@0.800.0": + version "0.800.0" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.800.0.tgz#dddf09764f1917a1f42fea61af2520b196cdbfd0" + integrity sha512-StheH+k4GCaHqmtDsHLSFmxu8SCDJVhZTXpz2zFAVaVXDh/ABS2Dt7I7SmEMGkUHcPA+u83sbZVBIacw2QfybQ== dependencies: - "@angular-devkit/core" "7.3.1" - "@angular-devkit/schematics" "7.3.1" + "@angular-devkit/core" "8.0.0" + "@angular-devkit/schematics" "8.0.0" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" - pacote "9.4.0" - rxjs "6.3.3" - semver "5.6.0" + pacote "9.5.0" + rxjs "6.4.0" + semver "6.0.0" semver-intersect "1.4.0" "@types/angular-animate@^1.5.10": @@ -422,6 +401,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + "@types/express-serve-static-core@*": version "4.0.55" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.55.tgz#f53868838a955f98b380819ec9134f5df7d9482f" @@ -436,6 +420,15 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/jasmine@*": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.2.tgz#6e6d4cb183cd55c7a1ad6270bced10fdd5367a3c" @@ -458,6 +451,11 @@ version "2.0.0" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/node@*": version "8.0.47" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.47.tgz#968e596f91acd59069054558a00708c445ca30c2" @@ -490,156 +488,174 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@webassemblyjs/ast@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.10.tgz#0cfc61d61286240b72fc522cb755613699eea40a" - integrity sha512-wTUeaByYN2EA6qVqhbgavtGc7fLTOx0glG2IBsFlrFG51uXIGlYBTyIZMf4SPLo3v1bgV/7lBN3l7Z0R6Hswew== +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/webpack-sources@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" + integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== dependencies: - "@webassemblyjs/helper-module-context" "1.7.10" - "@webassemblyjs/helper-wasm-bytecode" "1.7.10" - "@webassemblyjs/wast-parser" "1.7.10" + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" -"@webassemblyjs/floating-point-hex-parser@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.10.tgz#ee63d729c6311a85863e369a473f9983f984e4d9" - integrity sha512-gMsGbI6I3p/P1xL2UxqhNh1ga2HCsx5VBB2i5VvJFAaqAjd2PBTRULc3BpTydabUQEGlaZCzEUQhLoLG7TvEYQ== - -"@webassemblyjs/helper-api-error@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.10.tgz#bfcb3bbe59775357475790a2ad7b289f09b2f198" - integrity sha512-DoYRlPWtuw3yd5BOr9XhtrmB6X1enYF0/54yNvQWGXZEPDF5PJVNI7zQ7gkcKfTESzp8bIBWailaFXEK/jjCsw== - -"@webassemblyjs/helper-buffer@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.10.tgz#0a8c624c67ad0b214d2e003859921a1988cb151b" - integrity sha512-+RMU3dt/dPh4EpVX4u5jxsOlw22tp3zjqE0m3ftU2tsYxnPULb4cyHlgaNd2KoWuwasCQqn8Mhr+TTdbtj3LlA== - -"@webassemblyjs/helper-code-frame@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.10.tgz#0ab7e22fad0241a173178c73976fc0edf50832ce" - integrity sha512-UiytbpKAULOEab2hUZK2ywXen4gWJVrgxtwY3Kn+eZaaSWaRM8z/7dAXRSoamhKFiBh1uaqxzE/XD9BLlug3gw== +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" + integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== dependencies: - "@webassemblyjs/wast-printer" "1.7.10" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" -"@webassemblyjs/helper-fsm@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.10.tgz#0915e7713fbbb735620a9d3e4fa3d7951f97ac64" - integrity sha512-w2vDtUK9xeSRtt5+RnnlRCI7wHEvLjF0XdnxJpgx+LJOvklTZPqWkuy/NhwHSLP19sm9H8dWxKeReMR7sCkGZA== +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" + integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== -"@webassemblyjs/helper-module-context@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.10.tgz#9beb83f72740f5ac8075313b5cac5e796510f755" - integrity sha512-yE5x/LzZ3XdPdREmJijxzfrf+BDRewvO0zl8kvORgSWmxpRrkqY39KZSq6TSgIWBxkK4SrzlS3BsMCv2s1FpsQ== +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" + integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== -"@webassemblyjs/helper-wasm-bytecode@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.10.tgz#797b1e734bbcfdea8399669cdc58308ef1c7ffc0" - integrity sha512-u5qy4SJ/OrxKxZqJ9N3qH4ZQgHaAzsopsYwLvoWJY6Q33r8PhT3VPyNMaJ7ZFoqzBnZlCcS/0f4Sp8WBxylXfg== +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== -"@webassemblyjs/helper-wasm-section@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.10.tgz#c0ea3703c615d7bc3e3507c3b7991c8767b2f20e" - integrity sha512-Ecvww6sCkcjatcyctUrn22neSJHLN/TTzolMGG/N7S9rpbsTZ8c6Bl98GpSpV77EvzNijiNRHBG0+JO99qKz6g== +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" + integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-buffer" "1.7.10" - "@webassemblyjs/helper-wasm-bytecode" "1.7.10" - "@webassemblyjs/wasm-gen" "1.7.10" + "@webassemblyjs/wast-printer" "1.8.5" -"@webassemblyjs/ieee754@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.10.tgz#62c1728b7ef0f66ef8221e2966a0afd75db430df" - integrity sha512-HRcWcY+YWt4+s/CvQn+vnSPfRaD4KkuzQFt5MNaELXXHSjelHlSEA8ZcqT69q0GTIuLWZ6JaoKar4yWHVpZHsQ== +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== + +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" + integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== + +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" + integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" + integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.10.tgz#167e0bb4b06d7701585772a73fba9f4df85439f6" - integrity sha512-og8MciYlA8hvzCLR71hCuZKPbVBfLQeHv7ImKZ4nlyxrYbG7uJHYtHiHu6OV9SqrGuD03H/HtXC4Bgdjfm9FHw== +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" + integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== dependencies: - "@xtuc/long" "4.2.1" + "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.10.tgz#b6728f5b6f50364abc155be029f9670e6685605a" - integrity sha512-Ng6Pxv6siyZp635xCSnH3mKmIFgqWPCcGdoo0GBYgyGdxu7cUj4agV7Uu1a8REP66UYUFXJLudeGgd4RvuJAnQ== +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== -"@webassemblyjs/wasm-edit@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.10.tgz#83fe3140f5a58f5a30b914702be9f0e59a399092" - integrity sha512-e9RZFQlb+ZuYcKRcW9yl+mqX/Ycj9+3/+ppDI8nEE/NCY6FoK8f3dKBcfubYV/HZn44b+ND4hjh+4BYBt+sDnA== +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" + integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-buffer" "1.7.10" - "@webassemblyjs/helper-wasm-bytecode" "1.7.10" - "@webassemblyjs/helper-wasm-section" "1.7.10" - "@webassemblyjs/wasm-gen" "1.7.10" - "@webassemblyjs/wasm-opt" "1.7.10" - "@webassemblyjs/wasm-parser" "1.7.10" - "@webassemblyjs/wast-printer" "1.7.10" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" -"@webassemblyjs/wasm-gen@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.10.tgz#4de003806ae29c97ab3707782469b53299570174" - integrity sha512-M0lb6cO2Y0PzDye/L39PqwV+jvO+2YxEG5ax+7dgq7EwXdAlpOMx1jxyXJTScQoeTpzOPIb+fLgX/IkLF8h2yw== +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" + integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-wasm-bytecode" "1.7.10" - "@webassemblyjs/ieee754" "1.7.10" - "@webassemblyjs/leb128" "1.7.10" - "@webassemblyjs/utf8" "1.7.10" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" -"@webassemblyjs/wasm-opt@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.10.tgz#d151e31611934a556c82789fdeec41a814993c2a" - integrity sha512-R66IHGCdicgF5ZliN10yn5HaC7vwYAqrSVJGjtJJQp5+QNPBye6heWdVH/at40uh0uoaDN/UVUfXK0gvuUqtVg== +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" + integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-buffer" "1.7.10" - "@webassemblyjs/wasm-gen" "1.7.10" - "@webassemblyjs/wasm-parser" "1.7.10" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" -"@webassemblyjs/wasm-parser@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.10.tgz#0367be7bf8f09e3e6abc95f8e483b9206487ec65" - integrity sha512-AEv8mkXVK63n/iDR3T693EzoGPnNAwKwT3iHmKJNBrrALAhhEjuPzo/lTE4U7LquEwyvg5nneSNdTdgrBaGJcA== +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" + integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-api-error" "1.7.10" - "@webassemblyjs/helper-wasm-bytecode" "1.7.10" - "@webassemblyjs/ieee754" "1.7.10" - "@webassemblyjs/leb128" "1.7.10" - "@webassemblyjs/utf8" "1.7.10" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" -"@webassemblyjs/wast-parser@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.10.tgz#058f598b52f730b23fc874d4775b6286b6247264" - integrity sha512-YTPEtOBljkCL0VjDp4sHe22dAYSm3ZwdJ9+2NTGdtC7ayNvuip1wAhaAS8Zt9Q6SW9E5Jf5PX7YE3XWlrzR9cw== +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" + integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/floating-point-hex-parser" "1.7.10" - "@webassemblyjs/helper-api-error" "1.7.10" - "@webassemblyjs/helper-code-frame" "1.7.10" - "@webassemblyjs/helper-fsm" "1.7.10" - "@xtuc/long" "4.2.1" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" + "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.7.10": - version "1.7.10" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.10.tgz#d817909d2450ae96c66b7607624d98a33b84223b" - integrity sha512-mJ3QKWtCchL1vhU/kZlJnLPuQZnlDOdZsyP0bbLWPGdYsQDnSBvyTLhzwBA3QAMlzEL9V4JHygEmK6/OTEyytA== +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" + integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/wast-parser" "1.7.10" - "@xtuc/long" "4.2.1" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" -"@xtuc/long@4.2.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8" +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== "@yarnpkg/lockfile@1.1.0": version "1.1.0" @@ -658,10 +674,6 @@ abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" -abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - accepts@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" @@ -683,21 +695,12 @@ accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" -acorn-dynamic-import@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" - dependencies: - acorn "^5.0.0" +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== -acorn@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" - -acorn@^5.6.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.2.tgz#91fa871883485d06708800318404e72bfb26dcc5" - -acorn@^6.1.1: +acorn@^6.0.5, acorn@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== @@ -735,19 +738,10 @@ ajv-keywords@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" -ajv@6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9" - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96" - integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg== +ajv@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== dependencies: fast-deep-equal "^2.0.1" fast-json-stable-stringify "^2.0.0" @@ -810,9 +804,10 @@ ansi-colors@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.1.0.tgz#dcfaacc90ef9187de413ec3ef8d5eb981a98808f" -ansi-escapes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== ansi-html@0.0.7: version "0.0.7" @@ -830,10 +825,10 @@ ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" -ansi-regex@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" - integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-styles@^1.1.0: version "1.1.0" @@ -910,10 +905,6 @@ arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -922,10 +913,6 @@ array-flatten@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -956,7 +943,7 @@ arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" -asap@~2.0.3: +asap@^2.0.0, asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -998,19 +985,16 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.0.tgz#ac3613b1da9bed1b47510bb4651b8931e47146c7" - -async@1.5.2, async@1.x, async@^1.4.0, async@^1.5.2: +async@1.5.2, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -1028,16 +1012,16 @@ atob@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" -autoprefixer@9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.3.1.tgz#71b622174de2b783d5fd99f9ad617b7a3c78443e" - integrity sha512-DY9gOh8z3tnCbJ13JIWaeQsoYncTGdsrgCceBaQSIL4nvdrLxgbRSBPevg2XbX7u4QCSfLheSJEEIUUSlkbx6Q== +autoprefixer@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.1.tgz#243b1267b67e7e947f28919d786b50d3bb0fb357" + integrity sha512-KJSzkStUl3wP0D5sdMlP82Q52JLy5+atf2MHAre48+ckWkXgixmfHyWmA77wFDy6jTHU6mIgXv6hAQ2mf1PjJQ== dependencies: - browserslist "^4.3.3" - caniuse-lite "^1.0.30000898" + browserslist "^4.5.4" + caniuse-lite "^1.0.30000957" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.5" + postcss "^7.0.14" postcss-value-parser "^3.3.1" aws-sign2@~0.6.0: @@ -1187,6 +1171,11 @@ big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" @@ -1235,6 +1224,22 @@ body-parser@1.18.2, body-parser@^1.16.1: raw-body "2.3.2" type-is "~1.6.15" +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + bonjour@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -1271,12 +1276,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - dependencies: - expand-range "^0.1.0" - braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" @@ -1301,7 +1300,7 @@ braces@^2.3.0: split-string "^3.0.2" to-regex "^3.0.1" -braces@^2.3.1: +braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" dependencies: @@ -1421,14 +1420,23 @@ browserify-zlib@^0.1.4: dependencies: pako "~0.2.0" -browserslist@^4.3.3: - version "4.4.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.1.tgz#42e828954b6b29a7a53e352277be429478a69062" - integrity sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A== +browserslist@4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.5.tgz#fe1a352330d2490d5735574c149a85bc18ef9b82" + integrity sha512-0QFO1r/2c792Ohkit5XI8Cm8pDtZxgNl2H6HU4mHrpYz7314pEYcsAVVatM0l/YmxPnEzh9VygXouj4gkFUTKA== dependencies: - caniuse-lite "^1.0.30000929" - electron-to-chromium "^1.3.103" - node-releases "^1.1.3" + caniuse-lite "^1.0.30000960" + electron-to-chromium "^1.3.124" + node-releases "^1.1.14" + +browserslist@^4.0.0, browserslist@^4.5.4: + version "4.5.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.6.tgz#ea42e8581ca2513fa7f371d4dd66da763938163d" + integrity sha512-o/hPOtbU9oX507lIqon+UvPYqpx3mHc8cV3QemSBTXwkG8gSQSK6UKvXcE/DcleU3+A59XTUHyCvZ5qGy8xVAg== + dependencies: + caniuse-lite "^1.0.30000963" + electron-to-chromium "^1.3.127" + node-releases "^1.1.17" browserstack@^1.5.1: version "1.5.1" @@ -1489,25 +1497,7 @@ bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^10.0.4: - version "10.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" - dependencies: - bluebird "^3.5.1" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^2.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.2" - ssri "^5.2.4" - unique-filename "^1.1.0" - y18n "^4.0.0" - -cacache@^11.0.1, cacache@^11.3.2: +cacache@^11.0.1, cacache@^11.3.1, cacache@^11.3.2: version "11.3.2" resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg== @@ -1564,21 +1554,10 @@ callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase@^1.0.2, camelcase@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" @@ -1587,15 +1566,25 @@ camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" -caniuse-lite@^1.0.30000898: - version "1.0.30000935" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000935.tgz#d1b59df00b46f4921bb84a8a34c1d172b346df59" - integrity sha512-1Y2uJ5y56qDt3jsDTdBHL1OqiImzjoQcBG6Yl3Qizq8mcc2SgCFpi+ZwLLqkztYnk9l87IYqRlNBnPSOTbFkXQ== +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30000929: - version "1.0.30000932" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz#d01763e9ce77810962ca7391ff827b5949ce4272" - integrity sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A== +caniuse-api@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000957, caniuse-lite@^1.0.30000960, caniuse-lite@^1.0.30000963: + version "1.0.30000967" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000967.tgz#a5039577806fccee80a04aaafb2c0890b1ee2f73" + integrity sha512-rUBIbap+VJfxTzrM4akJ00lkvVb5/n5v3EGXfWzSH5zT8aJmGzjA8HWhJ4U6kCpzxozUSnB+yvAYDRPY6mRpgQ== canonical-path@1.0.0: version "1.0.0" @@ -1662,7 +1651,7 @@ chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" -chokidar@1.7.0, chokidar@^1.4.2: +chokidar@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -1677,25 +1666,6 @@ chokidar@1.7.0, chokidar@^1.4.2: optionalDependencies: fsevents "^1.0.0" -chokidar@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" - dependencies: - anymatch "^2.0.0" - async-each "^1.0.0" - braces "^2.3.0" - glob-parent "^3.1.0" - inherits "^2.0.1" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - lodash.debounce "^4.0.8" - normalize-path "^2.1.1" - path-is-absolute "^1.0.0" - readdirp "^2.0.0" - upath "^1.0.5" - optionalDependencies: - fsevents "^1.2.2" - chokidar@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.0.tgz#6686313c541d3274b2a5c01233342037948c911b" @@ -1731,6 +1701,25 @@ chokidar@^2.0.2, chokidar@^2.0.3: optionalDependencies: fsevents "^1.1.2" +chokidar@^2.1.1, chokidar@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" + integrity sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" @@ -1757,10 +1746,6 @@ circular-dependency-plugin@5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz#da168c0b37e7b43563fb9f912c1c007c213389ef" -circular-json@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.5.tgz#64182ef359042d37cd8e767fc9de878b1e9447d3" - class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -1860,12 +1845,6 @@ colors@1.1.2, colors@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" -combine-lists@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - dependencies: - lodash "^4.5.0" - combined-stream@1.0.6, combined-stream@~1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" @@ -1886,14 +1865,15 @@ commander@^2.12.1: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" +commander@^2.19.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + commander@^2.2.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" -commander@~2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" - commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -1922,22 +1902,24 @@ component-inherit@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" -compressible@~2.0.11: - version "2.0.12" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== dependencies: - mime-db ">= 1.30.0 < 2" + mime-db ">= 1.40.0 < 2" -compression@^1.5.2: - version "1.7.1" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db" +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== dependencies: - accepts "~1.3.4" + accepts "~1.3.5" bytes "3.0.0" - compressible "~2.0.11" + compressible "~2.0.16" debug "2.6.9" - on-headers "~1.0.1" - safe-buffer "5.1.1" + on-headers "~1.0.2" + safe-buffer "5.1.2" vary "~1.1.2" concat-map@0.0.1: @@ -1965,10 +1947,15 @@ concurrently@^3.0.0: supports-color "^3.2.3" tree-kill "^1.1.0" -connect-history-api-fallback@^1.1.0, connect-history-api-fallback@^1.2.0, connect-history-api-fallback@^1.3.0: +connect-history-api-fallback@^1.1.0, connect-history-api-fallback@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.4.0.tgz#3db24f973f4b923b0e82f619ce0df02411ca623d" +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + connect-logger@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/connect-logger/-/connect-logger-0.0.1.tgz#4d999978a1d20bb4608e7cd434d741652255174b" @@ -2054,18 +2041,27 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -copy-webpack-plugin@4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.4.tgz#f2b2782b3cd5225535c3dc166a80067e7d940f27" +copy-webpack-plugin@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.2.tgz#56186dfddbf9aa1b29c97fa4c796c1be98870da4" + integrity sha512-7nC7EynPrnBTtBwwbG1aTqrfNS1aTb9eEjSmQDqFtKAsJrR3uDb+pCDIFT2LzhW+SgGJxQcYzThrmXzzZ720uw== dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" + cacache "^11.3.1" + find-cache-dir "^2.0.0" + glob-parent "^3.1.0" globby "^7.1.1" is-glob "^4.0.0" loader-utils "^1.1.0" minimatch "^3.0.4" - p-limit "^1.0.0" + normalize-path "^3.0.0" + p-limit "^2.1.0" serialize-javascript "^1.4.0" + webpack-log "^2.0.0" + +core-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738" + integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew== core-js@^2.2.0, core-js@^2.4.0: version "2.5.1" @@ -2123,21 +2119,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -2179,12 +2160,6 @@ css-parse@1.7.x: version "1.7.0" resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.7.0.tgz#321f6cf73782a6ff751111390fc05e2c657d8c9b" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - dependencies: - array-find-index "^1.0.1" - custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -2203,9 +2178,10 @@ date-fns@^1.23.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" -date-format@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" +date-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.0.0.tgz#7cf7b172f1ec564f0003b39ea302c5498fb98c8f" + integrity sha512-M6UqVvZVgFYqZL1SfHsRGIQSz3ZL+qgbsV5Lp1Vj61LZVYuEwcMXYay7DRDtYs2HQQBK5hQtQ0fD9aEJ89V0LA== date-now@^0.1.4: version "0.1.4" @@ -2247,16 +2223,29 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.2.5: +debug@^3.0.0, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" -decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: +debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= decamelize@^2.0.0: version "2.0.0" @@ -2280,15 +2269,12 @@ deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - -default-gateway@^2.6.0: - version "2.7.2" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== dependencies: - execa "^0.10.0" + execa "^1.0.0" ip-regex "^2.1.0" default-require-extensions@^2.0.0: @@ -2328,16 +2314,18 @@ del@^2.2.0: pinkie-promise "^2.0.0" rimraf "^2.2.8" -del@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" +del@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== dependencies: + "@types/glob" "^7.1.1" globby "^6.1.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - p-map "^1.1.1" - pify "^3.0.0" - rimraf "^2.2.8" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" delayed-stream@~1.0.0: version "1.0.0" @@ -2380,14 +2368,23 @@ detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" -detect-node@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== dev-ip@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" @@ -2449,9 +2446,10 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -domino@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.0.tgz#653ba7d331441113b42e40ba05f24253ec86e02e" +domino@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.3.tgz#0ca1ad02cbd316ebe2e99e0ac9fb0010407d4601" + integrity sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg== duplexify@^3.1.2, duplexify@^3.4.2: version "3.5.1" @@ -2480,23 +2478,24 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -ecstatic@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-1.4.1.tgz#32cb7b6fa2e290d58668674d115e8f0c3d567d6a" +ecstatic@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.1.tgz#b15b5b036c2233defc78d7bacbd8765226c95577" + integrity sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ== dependencies: - he "^0.5.0" - mime "^1.2.11" + he "^1.1.1" + mime "^1.6.0" minimist "^1.1.0" - url-join "^1.0.0" + url-join "^2.0.5" ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -electron-to-chromium@^1.3.103: - version "1.3.109" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz#ee04a55a5157a5580a5ea88e526b02c84a3a7bc8" - integrity sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow== +electron-to-chromium@^1.3.124, electron-to-chromium@^1.3.127: + version "1.3.133" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.133.tgz#c47639c19b91feee3e22fad69f5556142007008c" + integrity sha512-lyoC8aoqbbDqsprb6aPdt9n3DpOZZzdz/T4IZKsR0/dkZIxnJVUjjcpOSwA66jPRIOyDAamCTAUqweU05kKNSg== elliptic@^6.0.0: version "6.4.0" @@ -2514,6 +2513,11 @@ emitter-steward@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/emitter-steward/-/emitter-steward-1.0.0.tgz#f3411ade9758a7565df848b2da0cbbd1b46cbd64" +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -2674,17 +2678,6 @@ escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^ version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -escodegen@1.8.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" - dependencies: - esprima "^2.7.1" - estraverse "^1.9.1" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.2.0" - eslint-scope@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" @@ -2692,10 +2685,6 @@ eslint-scope@^4.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" -esprima@2.7.x, esprima@^2.7.1: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -2707,10 +2696,6 @@ esrecurse@^4.1.0: estraverse "^4.1.0" object-assign "^4.0.1" -estraverse@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" - estraverse@^4.1.0, estraverse@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -2737,6 +2722,11 @@ eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" +eventemitter3@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -2767,12 +2757,13 @@ execa@^0.10.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" + cross-spawn "^6.0.0" + get-stream "^4.0.0" is-stream "^1.1.0" npm-run-path "^2.0.0" p-finally "^1.0.0" @@ -2783,14 +2774,6 @@ exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" -expand-braces@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -2809,13 +2792,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" @@ -2866,38 +2842,39 @@ express@^4.14.1: utils-merge "1.0.1" vary "~1.1.2" -express@^4.16.2: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" +express@^4.16.4: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== dependencies: - accepts "~1.3.4" + accepts "~1.3.5" array-flatten "1.1.1" - body-parser "1.18.2" + body-parser "1.18.3" content-disposition "0.5.2" content-type "~1.0.4" cookie "0.3.1" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" + depd "~1.1.2" + encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.1.0" + finalhandler "1.1.1" fresh "0.5.2" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" parseurl "~1.3.2" path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" + proxy-addr "~2.0.4" + qs "6.5.2" range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" + statuses "~1.4.0" + type-is "~1.6.16" utils-merge "1.0.1" vary "~1.1.2" @@ -2922,9 +2899,10 @@ extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" -external-editor@^3.0.0: +external-editor@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== dependencies: chardet "^0.7.0" iconv-lite "^0.4.24" @@ -2965,10 +2943,6 @@ fast-json-stable-stringify@2.0.0, fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -2992,9 +2966,10 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -file-loader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-2.0.0.tgz#39749c82f020b9e85901dcff98e8004e6401cfde" +file-loader@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== dependencies: loader-utils "^1.0.2" schema-utils "^1.0.0" @@ -3051,18 +3026,6 @@ finalhandler@1.0.6: statuses "~1.3.1" unpipe "~1.0.0" -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" - dependencies: - debug "2.6.9" - encodeurl "~1.0.1" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.3.1" - unpipe "~1.0.0" - finalhandler@1.1.1: version "1.1.1" resolved "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" @@ -3075,14 +3038,6 @@ finalhandler@1.1.1: statuses "~1.4.0" unpipe "~1.0.0" -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - find-cache-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" @@ -3098,18 +3053,17 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - dependencies: - locate-path "^2.0.0" - find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" dependencies: locate-path "^3.0.0" +flatted@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" + integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== + flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -3117,6 +3071,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +follow-redirects@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== + dependencies: + debug "^3.2.6" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -3212,6 +3173,15 @@ fs-extra@3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" +fs-extra@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -3245,12 +3215,13 @@ fsevents@^1.1.2: nan "^2.3.0" node-pre-gyp "^0.6.39" -fsevents@^1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + nan "^2.12.1" + node-pre-gyp "^0.12.0" fstream-ignore@^1.0.5: version "1.0.5" @@ -3282,12 +3253,6 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gaze@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" - dependencies: - globule "^1.0.0" - genfun@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -3297,15 +3262,16 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" -get-stream@^4.1.0: +get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -3364,27 +3330,7 @@ glob@7.1.3, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3439,14 +3385,6 @@ globby@^7.1.1: pify "^3.0.0" slash "^1.0.0" -globule@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" - dependencies: - glob "~7.1.1" - lodash "~4.17.4" - minimatch "~3.0.2" - graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3456,19 +3394,10 @@ graceful-fs@^4.1.15: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -handle-thing@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" - -handlebars@^4.0.1: - version "4.0.11" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" - dependencies: - async "^1.4.0" - optimist "^0.6.1" - source-map "^0.4.4" - optionalDependencies: - uglify-js "^2.6" +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.0.11: version "4.0.12" @@ -3624,9 +3553,10 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" +he@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hmac-drbg@^1.0.0: version "1.0.1" @@ -3661,9 +3591,10 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -html-entities@^1.2.0: +html-entities@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= http-cache-semantics@^3.8.1: version "3.8.1" @@ -3683,6 +3614,16 @@ http-errors@1.6.2, http-errors@~1.6.1, http-errors@~1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-errors@1.6.3, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" @@ -3703,14 +3644,15 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy-middleware@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" +http-proxy-middleware@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== dependencies: - http-proxy "^1.16.2" + http-proxy "^1.17.0" is-glob "^4.0.0" - lodash "^4.17.5" - micromatch "^3.1.9" + lodash "^4.17.11" + micromatch "^3.1.10" http-proxy@1.15.2: version "1.15.2" @@ -3719,24 +3661,34 @@ http-proxy@1.15.2: eventemitter3 "1.x.x" requires-port "1.x.x" -http-proxy@^1.13.0, http-proxy@^1.16.2, http-proxy@^1.8.1: +http-proxy@^1.13.0, http-proxy@^1.8.1: version "1.16.2" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" dependencies: eventemitter3 "1.x.x" requires-port "1.x.x" -http-server@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.9.0.tgz#8f1b06bdc733618d4dc42831c7ba1aff4e06001a" +http-proxy@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" + integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + dependencies: + eventemitter3 "^3.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-server@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b" + integrity sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w== dependencies: colors "1.0.3" corser "~2.0.0" - ecstatic "^1.4.0" + ecstatic "^3.0.0" http-proxy "^1.8.1" opener "~1.4.0" optimist "0.6.x" - portfinder "0.4.x" + portfinder "^1.0.13" union "~0.4.3" http-signature@~1.1.0: @@ -3777,6 +3729,13 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3840,16 +3799,6 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - dependencies: - repeating "^2.0.0" - indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -3878,31 +3827,32 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" -inquirer@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52" - integrity sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg== +inquirer@6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7" + integrity sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA== dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" + ansi-escapes "^3.2.0" + chalk "^2.4.2" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^3.0.0" + external-editor "^3.0.3" figures "^2.0.0" - lodash "^4.17.10" + lodash "^4.17.11" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^6.1.0" + rxjs "^6.4.0" string-width "^2.1.0" - strip-ansi "^5.0.0" + strip-ansi "^5.1.0" through "^2.3.6" -internal-ip@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" +internal-ip@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== dependencies: - default-gateway "^2.6.0" - ipaddr.js "^1.5.2" + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" interpret@^1.0.0, interpret@^1.1.0: version "1.1.0" @@ -3930,17 +3880,14 @@ ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" - ipaddr.js@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" -ipaddr.js@^1.5.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.1.tgz#fa4b79fa47fd3def5e3b159825161c0a519c9427" +ipaddr.js@1.9.0, ipaddr.js@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== is-accessor-descriptor@^0.1.6: version "0.1.6" @@ -4079,10 +4026,6 @@ is-number-like@^1.0.3: dependencies: lodash.isfinite "^3.3.2" -is-number@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -4115,18 +4058,42 @@ is-path-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" +is-path-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.1.0.tgz#2e0c7e463ff5b7a0eb60852d851a6809347a124c" + integrity sha512-Sc5j3/YnM8tDeyCsVeKlm/0p95075DyLmDEIkSgQ7mXkrOX+uTCtmQFm0CYzVyJwcCCmO3k8qfJt17SxQwB5Zw== + is-path-in-cwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" dependencies: is-path-inside "^1.0.0" +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + is-path-inside@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" dependencies: path-is-inside "^1.0.1" +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -4287,36 +4254,19 @@ istanbul-reports@^2.0.1: dependencies: handlebars "^4.0.11" -istanbul@0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" - dependencies: - abbrev "1.0.x" - async "1.x" - escodegen "1.8.x" - esprima "2.7.x" - glob "^5.0.15" - handlebars "^4.0.1" - js-yaml "3.x" - mkdirp "0.5.x" - nopt "3.x" - once "1.x" - resolve "1.1.x" - supports-color "^3.1.0" - which "^1.1.1" - wordwrap "^1.0.0" +jasmine-core@^3.3, jasmine-core@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.4.0.tgz#2a74618e966026530c3518f03e9f845d26473ce3" + integrity sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg== jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" -jasmine-core@~2.99.1: - version "2.99.1" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.99.1.tgz#e6400df1e6b56e130b61c4bcd093daa7f6e8ca15" - -jasmine-marbles@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.4.0.tgz#de72331d189d4968e4b1e78b638e51654040c755" +jasmine-marbles@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.5.0.tgz#5d4c51082fcf619bb8dbc85583cb11718a32b200" + integrity sha512-hkSYy7VJpcxaKE48s/CasVpGyheElp5ZegguFi5kpYAaUWsyOko6RnMZS1kv14ThMtlJVNqCW5z16f1q6HqbEg== dependencies: lodash "^4.5.0" @@ -4338,10 +4288,6 @@ jasminewd2@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" -js-base64@^2.1.8: - version "2.3.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" - js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -4350,13 +4296,6 @@ js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@3.x: - version "3.11.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^3.12.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" @@ -4364,9 +4303,10 @@ js-yaml@^3.12.0, js-yaml@^3.9.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.7.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" +js-yaml@^3.13.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -4417,12 +4357,26 @@ json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonfile@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -4475,36 +4429,41 @@ karma-jasmine@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf" -karma-jasmine@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.2.tgz#394f2b25ffb4a644b9ada6f22d443e2fd08886c3" +karma-jasmine@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-2.0.1.tgz#26e3e31f2faf272dd80ebb0e1898914cc3a19763" + integrity sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA== + dependencies: + jasmine-core "^3.3" -karma-source-map-support@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz#36dd4d8ca154b62ace95696236fae37caf0a7dde" +karma-source-map-support@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" + integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== dependencies: source-map-support "^0.5.5" -karma@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/karma/-/karma-3.0.0.tgz#6da83461a8a28d8224575c3b5b874e271b4730c3" +karma@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-4.1.0.tgz#d07387c9743a575b40faf73e8a3eb5421c2193e1" + integrity sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" + braces "^2.3.2" chokidar "^2.0.3" colors "^1.1.0" - combine-lists "^1.0.0" connect "^3.6.0" core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" - expand-braces "^0.1.1" + flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" isbinaryfile "^3.0.0" - lodash "^4.17.4" - log4js "^3.0.0" + lodash "^4.17.11" + log4js "^4.0.0" mime "^2.3.1" minimatch "^3.0.2" optimist "^0.6.1" @@ -4515,11 +4474,12 @@ karma@~3.0.0: socket.io "2.1.1" source-map "^0.6.1" tmp "0.0.33" - useragent "2.2.1" + useragent "2.3.0" -killable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -4571,9 +4531,10 @@ less-loader@4.1.0: loader-utils "^1.1.0" pify "^3.0.0" -less@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/less/-/less-3.8.1.tgz#f31758598ef5a1930dd4caefa9e4340641e71e1d" +less@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/less/-/less-3.9.0.tgz#b7511c43f37cf57dc87dffd9883ec121289b1474" + integrity sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w== dependencies: clone "^2.1.2" optionalDependencies: @@ -4586,17 +4547,12 @@ less@3.8.1: request "^2.83.0" source-map "~0.6.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -license-webpack-plugin@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.0.2.tgz#9d34b521cb7fca8527945310b05be6ef0248b687" +license-webpack-plugin@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.1.1.tgz#f0ab760f7f301c76f5af52e480f320656b5721bb" + integrity sha512-TiarZIg5vkQ2rGdYJn2+5YxO/zqlqjpK5IVglr7OfmrN1sBCakS+PQrsP2uC5gtve1ZDb9WMSUMlmHDQ0FoW4w== dependencies: + "@types/webpack-sources" "^0.1.5" webpack-sources "^1.2.0" lie@~3.1.0: @@ -4629,20 +4585,20 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" -loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -4659,13 +4615,6 @@ localtunnel@1.8.3: request "2.81.0" yargs "3.29.0" -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -4673,35 +4622,33 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash.assign@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - -lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - lodash.isfinite@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3" -lodash.mergewith@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + lodash@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.16.2, lodash@^4.17.4, lodash@^4.5.0, lodash@^4.5.1, lodash@~4.17.4: +lodash@^4.11.1, lodash@^4.16.2, lodash@^4.17.4, lodash@^4.5.0, lodash@^4.5.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4709,23 +4656,30 @@ lodash@^4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" +lodash@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + lodash@^4.17.5: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -log4js@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.5.tgz#b80146bfebad68b430d4f3569556d8a6edfef303" +log4js@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.1.1.tgz#48ebed51e6989557b8a991c48f01879e82773545" + integrity sha512-tSQUF9bBMdcBtuLD6vD7hBM9Ci6Lng/NVHZEq4YbuRGo7ObmLiZuhxz33HKAmJItit74pAjvZgirqYX2LRaoGA== dependencies: - circular-json "^0.5.5" - date-format "^1.2.0" - debug "^3.1.0" + date-format "^2.0.0" + debug "^4.1.1" + flatted "^2.0.0" rfdc "^1.1.2" - streamroller "0.7.0" + streamroller "^1.0.4" -loglevel@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.5.1.tgz#189078c94ab9053ee215a0acdbf24244ea0f6502" +loglevel@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" + integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= longest@^1.0.1: version "1.0.1" @@ -4737,25 +4691,7 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lru-cache@2.2.x: - version "2.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" - -lru-cache@^4.0.1, lru-cache@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^4.1.2: +lru-cache@4.1.x, lru-cache@^4.1.2: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== @@ -4777,19 +4713,19 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -magic-string@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.0.tgz#1f3696f9931ff0a1ed4c132250529e19cad6759b" - dependencies: - sourcemap-codec "^1.4.1" - -magic-string@^0.25.1: +magic-string@0.25.2, magic-string@^0.25.1: version "0.25.2" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.2.tgz#139c3a729515ec55e96e69e82a11fe890a293ad9" integrity sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg== dependencies: sourcemap-codec "^1.4.4" +magic-string@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.0.tgz#1f3696f9931ff0a1ed4c132250529e19cad6759b" + dependencies: + sourcemap-codec "^1.4.1" + make-dir@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" @@ -4823,6 +4759,11 @@ make-fetch-happen@^4.0.1: socks-proxy-agent "^4.0.0" ssri "^6.0.0" +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== + map-age-cleaner@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" @@ -4833,10 +4774,6 @@ map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -4854,12 +4791,6 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" - dependencies: - mimic-fn "^1.0.0" - mem@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" @@ -4868,28 +4799,13 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" -memory-fs@^0.4.0, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" dependencies: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -4916,7 +4832,7 @@ micromatch@2.3.11, micromatch@^2.1.5: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.10, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.1.10, micromatch@^3.1.8: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -4959,9 +4875,10 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -"mime-db@>= 1.30.0 < 2": - version "1.31.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.31.0.tgz#a49cd8f3ebf3ed1a482b60561d9105ad40ca74cb" +"mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== mime-db@~1.30.0: version "1.30.0" @@ -4991,7 +4908,7 @@ mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" -mime@1.4.1, mime@^1.2.11, mime@^1.4.1: +mime@1.4.1, mime@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" @@ -4999,6 +4916,11 @@ mime@1.4.1, mime@^1.2.11, mime@^1.4.1: version "2.0.3" resolved "https://registry.yarnpkg.com/mime/-/mime-2.0.3.tgz#4353337854747c48ea498330dc034f9f4bbbcc0b" +mime@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -5007,12 +4929,13 @@ mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" -mini-css-extract-plugin@0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.4.tgz#c10410a004951bd3cedac1da69053940fccb625d" - integrity sha512-o+Jm+ocb0asEngdM6FsZWtZsRzA8koFUudIDwYUfl94M3PejPHG7Vopw5hN9V8WsMkSFpm3tZP3Fesz89EyrfQ== +mini-css-extract-plugin@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9" + integrity sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw== dependencies: loader-utils "^1.1.0" + normalize-url "^2.0.1" schema-utils "^1.0.0" webpack-sources "^1.1.0" @@ -5024,7 +4947,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -5034,7 +4957,7 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.0, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -5070,21 +4993,6 @@ minizlib@^1.1.1: dependencies: minipass "^2.2.1" -mississippi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^2.0.1" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -5179,18 +5087,15 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" +nan@^2.12.1: + version "2.13.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" + integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw== nan@^2.3.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" -nan@^2.9.2: - version "2.11.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" - nanomatch@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" @@ -5253,26 +5158,10 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" -node-forge@0.6.33: - version "0.6.33" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" - -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" +node-forge@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" + integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== node-libs-browser@^2.0.0: version "2.0.0" @@ -5302,9 +5191,10 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -5348,39 +5238,14 @@ node-pre-gyp@^0.6.39: tar "^2.2.1" tar-pack "^3.4.0" -node-releases@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.6.tgz#47d160033e24a64e79487a62de63cf691052ec54" - integrity sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg== +node-releases@^1.1.14, node-releases@^1.1.17: + version "1.1.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.18.tgz#cc98fd75598a324a77188ebddf6650e9cbd8b1d5" + integrity sha512-/mnVgm6u/8OwlIsoyRXtTI0RfQcxZoAZbdwyXap0EeWwcOpDDymyCHM2/aR9XKmHXrvizHoPAOs0pcbiJ6RUaA== dependencies: semver "^5.3.0" -node-sass@4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.10.0.tgz#dcc2b364c0913630945ccbf7a2bbf1f926effca4" - integrity sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^3.0.0" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - in-publish "^2.0.0" - lodash.assign "^4.2.0" - lodash.clonedeep "^4.3.2" - lodash.mergewith "^4.6.0" - meow "^3.7.0" - mkdirp "^0.5.1" - nan "^2.10.0" - node-gyp "^3.8.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "^2.2.4" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -"nopt@2 || 3", nopt@3.0.x, nopt@3.x: +nopt@3.0.x: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" dependencies: @@ -5393,7 +5258,17 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: +normalize-package-data@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^2.3.2: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" dependencies: @@ -5418,10 +5293,24 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" +normalize-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + npm-bundled@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" @@ -5478,7 +5367,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: +npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: @@ -5550,21 +5439,27 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -obuf@^1.0.0, obuf@^1.1.1: +obuf@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" +obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" dependencies: ee-first "1.1.1" -on-headers@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -5580,6 +5475,13 @@ open@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc" +open@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.2.0.tgz#7cf92cb961b5d8498b071e64098bf5e27f57230c" + integrity sha512-Vxf6HJkwrqmvh9UAID3MnMYXntbTxKLOSfOnO7LJdzPf3NE3KQYFNV0/Lcz2VAndbRFil58XVCyh8tiX11fiYw== + dependencies: + is-wsl "^1.1.0" + opener@~1.4.0: version "1.4.3" resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" @@ -5595,22 +5497,10 @@ opn@4.0.2: object-assign "^4.0.1" pinkie-promise "^2.0.0" -opn@5.3.0: - version "5.3.0" - resolved "http://registry.npmjs.org/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" - dependencies: - is-wsl "^1.1.0" - -opn@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" - integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== - dependencies: - is-wsl "^1.1.0" - -opn@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.1.0.tgz#72ce2306a17dbea58ff1041853352b4a8fc77519" +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== dependencies: is-wsl "^1.1.0" @@ -5621,17 +5511,6 @@ optimist@0.6.x, optimist@^0.6.1, optimist@~0.6.0: minimist "~0.0.1" wordwrap "~0.0.2" -optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" @@ -5657,14 +5536,6 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" - dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" - os-locale@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" @@ -5673,11 +5544,20 @@ os-locale@^3.0.0: lcid "^2.0.0" mem "^4.0.0" +os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -osenv@0, osenv@^0.1.4: +osenv@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" dependencies: @@ -5703,27 +5583,18 @@ p-is-promise@^1.1.0: version "1.1.0" resolved "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" -p-limit@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" - dependencies: - p-try "^1.0.0" - -p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" - p-limit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" dependencies: p-try "^2.0.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" +p-limit@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== dependencies: - p-limit "^1.1.0" + p-try "^2.0.0" p-locate@^3.0.0: version "3.0.0" @@ -5731,22 +5602,19 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== p-try@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" -pacote@9.4.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.4.0.tgz#af979abdeb175cd347c3e33be3241af1ed254807" - integrity sha512-WQ1KL/phGMkedYEQx9ODsjj7xvwLSpdFJJdEXrLyw5SILMxcTNt5DTxT2Z93fXuLFYJBlZJdnwdalrQdB/rX5w== +pacote@9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.0.tgz#85f3013a3f6dd51c108b0ccabd3de8102ddfaeda" + integrity sha512-aUplXozRbzhaJO48FaaeClmN+2Mwt741MC6M3bevIGZwdCaP7frXzbUOfOWa91FPHoLITzG0hYaKY363lxO3bg== dependencies: bluebird "^3.5.3" cacache "^11.3.2" @@ -5876,9 +5744,10 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1: +path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" @@ -5905,12 +5774,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -5943,6 +5806,11 @@ pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -5953,36 +5821,16 @@ pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - dependencies: - find-up "^2.1.0" - pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" dependencies: find-up "^3.0.0" -portfinder@0.4.x: - version "0.4.0" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-0.4.0.tgz#a3ffadffafe4fb98e0601a85eda27c27ce84ca1e" - dependencies: - async "0.9.0" - mkdirp "0.5.x" - -portfinder@1.0.17: - version "1.0.17" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" - dependencies: - async "^1.5.2" - debug "^2.2.0" - mkdirp "0.5.x" - -portfinder@^1.0.9: - version "1.0.13" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" +portfinder@^1.0.13, portfinder@^1.0.20: + version "1.0.20" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" + integrity sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw== dependencies: async "^1.5.2" debug "^2.2.0" @@ -5999,9 +5847,10 @@ posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" -postcss-import@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.0.tgz#149f96a4ef0b27525c419784be8517ebd17e92c5" +postcss-import@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== dependencies: postcss "^7.0.1" postcss-value-parser "^3.2.3" @@ -6033,15 +5882,7 @@ postcss-value-parser@^3.3.1: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss@7.0.5, postcss@^7.0.0, postcss@^7.0.1: - version "7.0.5" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.5.0" - -postcss@^7.0.5: +postcss@7.0.14: version "7.0.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== @@ -6050,9 +5891,27 @@ postcss@^7.0.5: source-map "^0.6.1" supports-color "^6.1.0" -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" +postcss@^7.0.0, postcss@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.5.0" + +postcss@^7.0.14: + version "7.0.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.16.tgz#48f64f1b4b558cb8b52c88987724359acb010da2" + integrity sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= preserve@^0.2.0: version "0.2.0" @@ -6062,10 +5921,6 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - process@^0.11.0: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -6116,13 +5971,6 @@ protractor@~5.4.0: webdriver-js-extender "2.1.0" webdriver-manager "^12.0.6" -proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.5.2" - proxy-addr@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" @@ -6130,6 +5978,14 @@ proxy-addr@~2.0.3: forwarded "~0.1.2" ipaddr.js "1.8.0" +proxy-addr@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -6159,13 +6015,6 @@ pump@^1.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pump@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -6217,6 +6066,11 @@ qs@6.5.1, "qs@>= 0.4.0", qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +qs@6.5.2, qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + qs@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404" @@ -6225,9 +6079,14 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" querystring-es3@^0.2.0: version "0.2.1" @@ -6268,9 +6127,23 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -raw-loader@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +raw-loader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405" + integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" rc@^1.1.7: version "1.2.2" @@ -6296,6 +6169,29 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" +read-package-json@^2.0.0: + version "2.0.13" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a" + integrity sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg== + dependencies: + glob "^7.1.1" + json-parse-better-errors "^1.0.1" + normalize-package-data "^2.0.0" + slash "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + +read-package-tree@5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.2.2.tgz#4b6a0ef2d943c1ea36a578214c9a7f6b7424f7a8" + integrity sha512-rW3XWUUkhdKmN2JKB4FL563YAgtINifso5KShykufR03nJ5loGFlkUMe1g/yxmqX073SoYYTsgXu7XdDinKZuA== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + once "^1.3.0" + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -6303,13 +6199,6 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -6318,15 +6207,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -6338,17 +6219,14 @@ read-pkg@^2.0.0: string_decoder "~1.0.3" util-deprecate "~1.0.1" -readable-stream@^2.3.0: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" +readable-stream@^3.0.6: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9" + integrity sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw== dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" readable-stream@~2.0.6: version "2.0.6" @@ -6361,6 +6239,16 @@ readable-stream@~2.0.6: string_decoder "~0.10.x" util-deprecate "~1.0.1" +readdir-scoped-modules@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" + integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c= + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -6370,19 +6258,21 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" dependencies: resolve "^1.1.6" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - reflect-metadata@^0.1.2: version "0.1.10" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.10.tgz#b4f83704416acad89988c9b15635d47e03b9344a" @@ -6418,10 +6308,6 @@ repeat-element@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" -repeat-string@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -6513,7 +6399,7 @@ request@^2.83.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@^2.87.0, request@^2.88.0: +request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" dependencies: @@ -6550,6 +6436,11 @@ require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + requires-port@1.x.x, requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -6568,10 +6459,6 @@ resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" -resolve@1.1.x: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - resolve@^1.1.6, resolve@^1.1.7: version "1.5.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" @@ -6629,6 +6516,13 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2. dependencies: glob "^7.0.5" +rimraf@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + rimraf@~2.2.6: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" @@ -6702,19 +6596,14 @@ rx@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" -rxjs@6.3.3: - version "6.3.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" +rxjs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" + integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== dependencies: tslib "^1.9.0" -rxjs@^6.1.0: - version "6.3.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.2.tgz#6a688b16c4e6e980e62ea805ec30648e1c60907f" - dependencies: - tslib "^1.9.0" - -rxjs@^6.5.1: +rxjs@^6.4.0, rxjs@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.1.tgz#f7a005a9386361921b8524f38f54cbf80e5d08f4" integrity sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg== @@ -6725,9 +6614,10 @@ safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -safe-buffer@^5.1.2: +safe-buffer@5.1.2, safe-buffer@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-regex@^1.1.0: version "1.1.0" @@ -6739,15 +6629,6 @@ safe-regex@^1.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^7.0.0" - sass-loader@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" @@ -6759,6 +6640,13 @@ sass-loader@7.1.0: pify "^3.0.0" semver "^5.5.0" +sass@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.19.0.tgz#5de82c713d4299fac57384ef5219534a37fe3e6c" + integrity sha512-8kzKCgxCzh8/zEn3AuRwzLWVSSFj8omkiGwqdJdeOufjM+I88dXxu9LYJ/Gw4rRTHXesN0r1AixBuqM6yLQUJw== + dependencies: + chokidar "^2.0.0" + saucelabs@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" @@ -6779,13 +6667,6 @@ schema-utils@^0.3.0: dependencies: ajv "^5.0.0" -schema-utils@^0.4.4, schema-utils@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" - dependencies: - ajv "^6.1.0" - ajv-keywords "^3.1.0" - schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -6794,13 +6675,6 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -6814,11 +6688,12 @@ selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: tmp "0.0.30" xml2js "^0.4.17" -selfsigned@^1.9.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52" +selfsigned@^1.10.4: + version "1.10.4" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" + integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== dependencies: - node-forge "0.6.33" + node-forge "0.7.5" semver-intersect@1.4.0: version "1.4.0" @@ -6830,22 +6705,19 @@ semver-intersect@1.4.0: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" -semver@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" - -semver@5.6.0, semver@^5.4.1, semver@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" - integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +semver@6.0.0, semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== semver@^5.0.0, semver@^5.0.1, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@^5.4.1, semver@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== send@0.15.2: version "0.15.2" @@ -6865,24 +6737,6 @@ send@0.15.2: range-parser "~1.2.0" statuses "~1.3.1" -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -6917,9 +6771,10 @@ serve-index@1.8.0: mime-types "~2.1.11" parseurl "~1.3.1" -serve-index@^1.7.2: +serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= dependencies: accepts "~1.3.4" batch "0.6.1" @@ -6938,15 +6793,6 @@ serve-static@1.12.2: parseurl "~1.3.1" send "0.15.2" -serve-static@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - serve-static@1.13.2: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" @@ -7214,6 +7060,13 @@ socks@~2.2.0: ip "^1.1.5" smart-buffer "4.0.2" +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" @@ -7252,7 +7105,15 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@0.5.9, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.6: +source-map-support@0.5.12, source-map-support@~0.5.10: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.5, source-map-support@^0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" dependencies: @@ -7283,12 +7144,6 @@ source-map@0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" -source-map@^0.4.2, source-map@^0.4.4, source-map@~0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - dependencies: - amdefine ">=0.0.4" - source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6, source-map@~0.5.1: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -7297,9 +7152,9 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" -source-map@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" +source-map@~0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" dependencies: amdefine ">=0.0.4" @@ -7330,33 +7185,33 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" -spdy-transport@^2.0.18: - version "2.0.20" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: - debug "^2.6.8" - detect-node "^2.0.3" + debug "^4.1.0" + detect-node "^2.0.4" hpack.js "^2.1.6" - obuf "^1.1.1" - readable-stream "^2.2.9" - safe-buffer "^5.0.1" - wbuf "^1.7.2" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" -spdy@^3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" +spdy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52" + integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q== dependencies: - debug "^2.6.8" - handle-thing "^1.2.5" + debug "^4.1.0" + handle-thing "^2.0.0" http-deceiver "^1.2.7" - safe-buffer "^5.0.1" select-hose "^2.0.0" - spdy-transport "^2.0.18" + spdy-transport "^3.0.0" -speed-measure-webpack-plugin@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.2.3.tgz#de170b5cefbfa1c039d95e639edd3ad50cfc7c48" - integrity sha512-p+taQ69VkRUXYMoZOx2nxV/Tz8tt79ahctoZJyJDHWP7fEYvwFNf5Pd73k5kZ6auu0pTsPNLEUwWpM8mCk85Zw== +speed-measure-webpack-plugin@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.1.tgz#69840a5cdc08b4638697dac7db037f595d7f36a0" + integrity sha512-qVIkJvbtS9j/UeZumbdfz0vg+QfG/zxonAjzefZrqzkr7xOncLVXkeGbTpzd1gjCBM4PmVNkWlkeTVhgskAGSQ== dependencies: chalk "^2.0.1" @@ -7384,12 +7239,6 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" -ssri@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" - dependencies: - safe-buffer "^5.1.1" - ssri@^6.0.0, ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" @@ -7413,16 +7262,15 @@ stats-webpack-plugin@0.7.0: version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + statuses@~1.3.0, statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" -stdout-stream@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" - dependencies: - readable-stream "^2.0.1" - stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" @@ -7458,14 +7306,21 @@ stream-throttle@^0.1.3: commander "^2.2.0" limiter "^1.0.5" -streamroller@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" +streamroller@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.4.tgz#d485c7624796d5e2eb34190c79afcbf006afb5e6" + integrity sha512-Wc2Gm5ygjSX8ZpW9J7Y9FwiSzTlKSvcl0FTTMd3rn7RoxDXpBW+xD9TY5sWL2n0UR61COB0LG1BQvN6nTUQbLQ== dependencies: - date-format "^1.2.0" + async "^2.6.1" + date-format "^2.0.0" debug "^3.1.0" - mkdirp "^0.5.1" - readable-stream "^2.3.0" + fs-extra "^7.0.0" + lodash "^4.17.10" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" @@ -7482,19 +7337,29 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" +string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== dependencies: safe-buffer "~5.1.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: safe-buffer "~5.1.0" @@ -7520,12 +7385,12 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f" - integrity sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow== +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: - ansi-regex "^4.0.0" + ansi-regex "^4.1.0" strip-bom@^2.0.0: version "2.0.0" @@ -7541,12 +7406,6 @@ strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - dependencies: - get-stdin "^4.0.1" - strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -7586,7 +7445,7 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.1.0, supports-color@^3.2.3: +supports-color@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" dependencies: @@ -7598,12 +7457,6 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" - dependencies: - has-flag "^2.0.0" - supports-color@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" @@ -7654,7 +7507,7 @@ tar-pack@^3.4.0: tar "^2.2.1" uid-number "^0.0.6" -tar@^2.0.0, tar@^2.2.1: +tar@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" dependencies: @@ -7694,26 +7547,28 @@ temp@^0.8.3: os-tmpdir "^1.0.0" rimraf "~2.2.6" -terser-webpack-plugin@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528" +terser-webpack-plugin@1.2.3, terser-webpack-plugin@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" + integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA== dependencies: cacache "^11.0.2" find-cache-dir "^2.0.0" schema-utils "^1.0.0" serialize-javascript "^1.4.0" source-map "^0.6.1" - terser "^3.8.1" + terser "^3.16.1" webpack-sources "^1.1.0" worker-farm "^1.5.2" -terser@^3.8.1: - version "3.8.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.8.2.tgz#48b880f949f8d038aca4dfd00a37c53d96ecf9fb" +terser@^3.16.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== dependencies: - commander "~2.17.1" + commander "^2.19.0" source-map "~0.6.1" - source-map-support "~0.5.6" + source-map-support "~0.5.10" tfunk@^3.0.1: version "3.1.0" @@ -7814,24 +7669,19 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" -tree-kill@1.2.0, tree-kill@^1.1.0: +tree-kill@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" + integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== + +tree-kill@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" -"true-case-path@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" - dependencies: - glob "^6.0.4" - ts-loader@^4.2.0: version "4.4.1" resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-4.4.1.tgz#c93a46eea430ebce1f790dfe438caefb8670d365" @@ -7863,9 +7713,10 @@ tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" -tslint@~5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" +tslint@~5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.15.0.tgz#6ffb180986d63afa1e531feb2a134dbf961e27d3" + integrity sha512-6bIEujKR21/3nyeoX2uBnE8s+tMXCQXhqMmaIPJpHmXJoBJPTLcI7/VHRtUwMhnLVdwLqqY3zmd8Dxqa5CVdJA== dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" @@ -7873,16 +7724,18 @@ tslint@~5.11.0: commander "^2.12.1" diff "^3.2.0" glob "^7.1.1" - js-yaml "^3.7.0" + js-yaml "^3.13.0" minimatch "^3.0.4" + mkdirp "^0.5.1" resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" - tsutils "^2.27.2" + tsutils "^2.29.0" -tsutils@^2.27.2: +tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== dependencies: tslib "^1.8.1" @@ -7900,12 +7753,6 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" - type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -7924,32 +7771,21 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -typescript@3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.4.tgz#aac4a08abecab8091a75f10842ffa0631818f785" + integrity sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA== -typescript@3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d" - integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg== - -typescript@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96" +typescript@~3.4.3: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== ua-parser-js@0.7.12: version "0.7.12" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb" -uglify-es@^3.3.4: - version "3.3.8" - resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.8.tgz#f2c68e6cff0d0f9dc9577e4da207151c2e753b7e" - dependencies: - commander "~2.13.0" - source-map "~0.6.1" - -uglify-js@^2.6, uglify-js@^2.6.1: +uglify-js@^2.6.1: version "2.8.29" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" dependencies: @@ -7969,19 +7805,6 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uglifyjs-webpack-plugin@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.4.tgz#5eec941b2e9b8538be0a20fc6eda25b14c7c1043" - dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - schema-utils "^0.4.5" - serialize-javascript "^1.4.0" - source-map "^0.6.1" - uglify-es "^3.3.4" - webpack-sources "^1.1.0" - worker-farm "^1.5.2" - uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -8036,6 +7859,15 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +universal-analytics@^0.4.20: + version "0.4.20" + resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.20.tgz#d6b64e5312bf74f7c368e3024a922135dbf24b03" + integrity sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw== + dependencies: + debug "^3.0.0" + request "^2.88.0" + uuid "^3.0.0" + universalify@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" @@ -8051,10 +7883,15 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -upath@^1.0.0, upath@^1.0.5: +upath@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + uri-js@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-3.0.2.tgz#f90b858507f81dea4dcfbb3c4c3dbfa2b557faaa" @@ -8071,9 +7908,10 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" -url-join@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-1.1.0.tgz#741c6c2f4596c4830d6718460920d0c92202dc78" +url-join@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" + integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= url-parse@^1.4.3: version "1.4.4" @@ -8098,14 +7936,15 @@ use@^2.0.0: isobject "^3.0.0" lazy-cache "^2.0.2" -useragent@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e" +useragent@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" + integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== dependencies: - lru-cache "2.2.x" + lru-cache "4.1.x" tmp "0.0.x" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -8182,12 +8021,19 @@ watchpack@^1.5.0: graceful-fs "^4.1.2" neo-async "^2.5.0" -wbuf@^1.1.0, wbuf@^1.7.2: +wbuf@^1.1.0: version "1.7.2" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" dependencies: minimalistic-assert "^1.0.0" +wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + web-animations-js@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/web-animations-js/-/web-animations-js-2.3.1.tgz#3a6d9bc15196377a90f8e2803fa5262165b04510" @@ -8237,49 +8083,51 @@ webpack-core@^0.6.8: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" - integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== +webpack-dev-middleware@3.6.2, webpack-dev-middleware@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.2.tgz#f37a27ad7c09cd7dc67cd97655413abaa1f55942" + integrity sha512-A47I5SX60IkHrMmZUlB0ZKSWi29TZTcPz7cha1Z75yYOsgWh/1AcPmQEbC8ZIbU3A1ytSv1PMU0PyPz2Lmz2jg== dependencies: - memory-fs "~0.4.1" + memory-fs "^0.4.1" mime "^2.3.1" range-parser "^1.0.3" webpack-log "^2.0.0" -webpack-dev-server@3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz#507411bee727ee8d2fdffdc621b66a64ab3dea2b" - integrity sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww== +webpack-dev-server@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.3.1.tgz#7046e49ded5c1255a82c5d942bcdda552b72a62d" + integrity sha512-jY09LikOyGZrxVTXK0mgIq9y2IhCoJ05848dKZqX1gAGLU1YDqgpOT71+W53JH/wI4v6ky4hm+KvSyW14JEs5A== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" - chokidar "^2.0.0" - compression "^1.5.2" - connect-history-api-fallback "^1.3.0" - debug "^3.1.0" - del "^3.0.0" - express "^4.16.2" - html-entities "^1.2.0" - http-proxy-middleware "~0.18.0" + chokidar "^2.1.5" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.0" + express "^4.16.4" + html-entities "^1.2.1" + http-proxy-middleware "^0.19.1" import-local "^2.0.0" - internal-ip "^3.0.1" + internal-ip "^4.2.0" ip "^1.1.5" - killable "^1.0.0" - loglevel "^1.4.1" - opn "^5.1.0" - portfinder "^1.0.9" + killable "^1.0.1" + loglevel "^1.6.1" + opn "^5.5.0" + portfinder "^1.0.20" schema-utils "^1.0.0" - selfsigned "^1.9.1" - serve-index "^1.7.2" + selfsigned "^1.10.4" + semver "^6.0.0" + serve-index "^1.9.1" sockjs "0.3.19" sockjs-client "1.3.0" - spdy "^3.4.1" - strip-ansi "^3.0.0" - supports-color "^5.1.0" - webpack-dev-middleware "3.4.0" + spdy "^4.0.0" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.6.2" webpack-log "^2.0.0" - yargs "12.0.2" + yargs "12.0.5" webpack-log@^2.0.0: version "2.0.0" @@ -8288,19 +8136,13 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@4.1.4: - version "4.1.4" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.4.tgz#0fde38eabf2d5fd85251c24a5a8c48f8a3f4eb7b" +webpack-merge@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4" + integrity sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw== dependencies: lodash "^4.17.5" -webpack-sources@1.2.0, webpack-sources@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.2.0.tgz#18181e0d013fce096faf6f8e6d41eeffffdceac2" - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - webpack-sources@1.3.0, webpack-sources@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" @@ -8316,23 +8158,30 @@ webpack-sources@^1.1.0: source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.2.0.tgz#18181e0d013fce096faf6f8e6d41eeffffdceac2" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + webpack-subresource-integrity@1.1.0-rc.6: version "1.1.0-rc.6" resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.6.tgz#37f6f1264e1eb378e41465a98da80fad76ab8886" dependencies: webpack-core "^0.6.8" -webpack@4.23.1: - version "4.23.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.23.1.tgz#db7467b116771ae020c58bdfe2a0822785bb8239" - integrity sha512-iE5Cu4rGEDk7ONRjisTOjVHv3dDtcFfwitSxT7evtYj/rANJpt1OuC/Kozh1pBa99AUBr1L/LsaNB+D9Xz3CEg== +webpack@4.30.0: + version "4.30.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.30.0.tgz#aca76ef75630a22c49fcc235b39b4c57591d33a9" + integrity sha512-4hgvO2YbAFUhyTdlR4FNyt2+YaYBYHavyzjCMbZzgglo02rlKi/pcsEzwCuCpsn1ryzIl1cq/u8ArIKu8JBYMg== dependencies: - "@webassemblyjs/ast" "1.7.10" - "@webassemblyjs/helper-module-context" "1.7.10" - "@webassemblyjs/wasm-edit" "1.7.10" - "@webassemblyjs/wasm-parser" "1.7.10" - acorn "^5.6.2" - acorn-dynamic-import "^3.0.0" + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.0.5" + acorn-dynamic-import "^4.0.0" ajv "^6.1.0" ajv-keywords "^3.1.0" chrome-trace-event "^1.0.0" @@ -8346,9 +8195,9 @@ webpack@4.23.1: mkdirp "~0.5.0" neo-async "^2.5.0" node-libs-browser "^2.0.0" - schema-utils "^0.4.4" + schema-utils "^1.0.0" tapable "^1.1.0" - uglifyjs-webpack-plugin "^1.2.4" + terser-webpack-plugin "^1.1.0" watchpack "^1.5.0" webpack-sources "^1.3.0" @@ -8387,7 +8236,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@1, which@^1.1.1, which@^1.2.1, which@^1.2.9: +which@^1.2.1, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: @@ -8422,10 +8271,6 @@ wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" -wordwrap@^1.0.0, wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -8437,6 +8282,13 @@ worker-farm@^1.5.2: errno "^0.1.4" xtend "^4.0.1" +worker-plugin@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-3.1.0.tgz#6311778f3514a87c273510ee3f809cc3fe161e6f" + integrity sha512-iQ9KTTmmN5fhfc2KMR7CcDblvcrg1QQ4pXymqZ3cRZF8L0890YLBcEqlIsGPdxoFwghyN8RA1pCEhCKuTF4Lkw== + dependencies: + loader-utils "^1.1.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" @@ -8520,30 +8372,35 @@ yargs-parser@^10.1.0: dependencies: camelcase "^4.1.0" +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.0.tgz#7016b6dd03e28e1418a510e258be4bff5a31138f" + integrity sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" dependencies: camelcase "^3.0.0" -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - dependencies: - camelcase "^3.0.0" - -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" - dependencies: - camelcase "^4.1.0" - -yargs@12.0.2, yargs@^12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" +yargs@12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== dependencies: cliui "^4.0.0" - decamelize "^2.0.0" + decamelize "^1.2.0" find-up "^3.0.0" get-caller-file "^1.0.1" os-locale "^3.0.0" @@ -8553,7 +8410,24 @@ yargs@12.0.2, yargs@^12.0.2: string-width "^2.0.0" which-module "^2.0.0" y18n "^3.2.1 || ^4.0.0" - yargs-parser "^10.1.0" + yargs-parser "^11.1.1" + +yargs@13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.1.0.tgz#b2729ce4bfc0c584939719514099d8a916ad2301" + integrity sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg== + dependencies: + cliui "^4.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.0.0" yargs@3.29.0: version "3.29.0" @@ -8585,41 +8459,22 @@ yargs@6.4.0: y18n "^3.2.1" yargs-parser "^4.1.0" -yargs@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" +yargs@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" + os-locale "^3.0.0" require-directory "^2.1.1" require-main-filename "^1.0.1" set-blocking "^2.0.0" string-width "^2.0.0" which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" - -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" yargs@~3.10.0: version "3.10.0" diff --git a/aio/tools/stackblitz-builder/builder.js b/aio/tools/stackblitz-builder/builder.js index 807b4c95e9..175dbaff74 100644 --- a/aio/tools/stackblitz-builder/builder.js +++ b/aio/tools/stackblitz-builder/builder.js @@ -221,9 +221,7 @@ class StackblitzBuilder { _encodeBase64(file) { // read binary data - var bitmap = fs.readFileSync(file); - // convert binary data to base64 encoded string - return Buffer(bitmap).toString('base64'); + return fs.readFileSync(file, { encoding: 'base64' }); } _existsSync(filename) { diff --git a/aio/tools/transforms/angular-api-package/index.js b/aio/tools/transforms/angular-api-package/index.js index be7d75cc8a..55baa84af0 100644 --- a/aio/tools/transforms/angular-api-package/index.js +++ b/aio/tools/transforms/angular-api-package/index.js @@ -9,197 +9,210 @@ const Package = require('dgeni').Package; const basePackage = require('../angular-base-package'); const typeScriptPackage = require('dgeni-packages/typescript'); -const { API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder } = require('../config'); +const {API_SOURCE_PATH, API_TEMPLATES_PATH, requireFolder} = require('../config'); -module.exports = new Package('angular-api', [basePackage, typeScriptPackage]) +module.exports = + new Package('angular-api', [basePackage, typeScriptPackage]) - // Register the processors - .processor(require('./processors/splitDescription')) - .processor(require('./processors/convertPrivateClassesToInterfaces')) - .processor(require('./processors/generateApiListDoc')) - .processor(require('./processors/addNotYetDocumentedProperty')) - .processor(require('./processors/mergeDecoratorDocs')) - .processor(require('./processors/extractDecoratedClasses')) - .processor(require('./processors/extractPipeParams')) - .processor(require('./processors/matchUpDirectiveDecorators')) - .processor(require('./processors/addMetadataAliases')) - .processor(require('./processors/computeApiBreadCrumbs')) - .processor(require('./processors/filterContainedDocs')) - .processor(require('./processors/processClassLikeMembers')) - .processor(require('./processors/markBarredODocsAsPrivate')) - .processor(require('./processors/filterPrivateDocs')) - .processor(require('./processors/computeSearchTitle')) - .processor(require('./processors/simplifyMemberAnchors')) - .processor(require('./processors/computeStability')) - .processor(require('./processors/removeInjectableConstructors')) - .processor(require('./processors/collectPackageContentDocs')) - .processor(require('./processors/processPackages')) - .processor(require('./processors/processNgModuleDocs')) - .processor(require('./processors/fixupRealProjectRelativePath')) - .processor(require('./processors/processAliasDocs')) + // Register the processors + .processor(require('./processors/splitDescription')) + .processor(require('./processors/convertPrivateClassesToInterfaces')) + .processor(require('./processors/generateApiListDoc')) + .processor(require('./processors/addNotYetDocumentedProperty')) + .processor(require('./processors/mergeDecoratorDocs')) + .processor(require('./processors/extractDecoratedClasses')) + .processor(require('./processors/extractPipeParams')) + .processor(require('./processors/matchUpDirectiveDecorators')) + .processor(require('./processors/addMetadataAliases')) + .processor(require('./processors/computeApiBreadCrumbs')) + .processor(require('./processors/filterContainedDocs')) + .processor(require('./processors/processClassLikeMembers')) + .processor(require('./processors/markBarredODocsAsPrivate')) + .processor(require('./processors/filterPrivateDocs')) + .processor(require('./processors/computeSearchTitle')) + .processor(require('./processors/simplifyMemberAnchors')) + .processor(require('./processors/computeStability')) + .processor(require('./processors/removeInjectableConstructors')) + .processor(require('./processors/collectPackageContentDocs')) + .processor(require('./processors/processPackages')) + .processor(require('./processors/processNgModuleDocs')) + .processor(require('./processors/fixupRealProjectRelativePath')) + .processor(require('./processors/processAliasDocs')) - /** - * These are the API doc types that will be rendered to actual files. - * This is a super set of the exported docs, since we convert some classes to - * more Angular specific API types, such as decorators and directives. - */ - .factory(function API_DOC_TYPES_TO_RENDER(EXPORT_DOC_TYPES) { - return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package']); - }) + /** + * These are the API doc types that will be rendered to actual files. + * This is a super set of the exported docs, since we convert some classes to + * more Angular specific API types, such as decorators and directives. + */ + .factory(function API_DOC_TYPES_TO_RENDER(EXPORT_DOC_TYPES) { + return EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe', 'package']); + }) - /** - * These are the doc types that are contained within other docs - */ - .factory(function API_CONTAINED_DOC_TYPES() { - return ['member', 'function-overload', 'get-accessor-info', 'set-accessor-info', 'parameter']; - }) + /** + * These are the doc types that are contained within other docs + */ + .factory(function API_CONTAINED_DOC_TYPES() { + return [ + 'member', 'function-overload', 'get-accessor-info', 'set-accessor-info', 'parameter' + ]; + }) - /** - * These are the doc types that are API docs, including ones that will be merged into container docs, - * such as members and overloads. - */ - .factory(function API_DOC_TYPES(API_DOC_TYPES_TO_RENDER, API_CONTAINED_DOC_TYPES) { - return API_DOC_TYPES_TO_RENDER.concat(API_CONTAINED_DOC_TYPES); - }) + /** + * These are the doc types that are API docs, including ones that will be merged into + * container docs, + * such as members and overloads. + */ + .factory(function API_DOC_TYPES(API_DOC_TYPES_TO_RENDER, API_CONTAINED_DOC_TYPES) { + return API_DOC_TYPES_TO_RENDER.concat(API_CONTAINED_DOC_TYPES); + }) - .factory(require('./readers/package-content')) + .factory(require('./readers/package-content')) - // Where do we get the source files? - .config(function(readTypeScriptModules, readFilesProcessor, collectExamples, tsParser, packageContentFileReader) { + // Where do we get the source files? + .config(function( + readTypeScriptModules, readFilesProcessor, collectExamples, tsParser, + packageContentFileReader) { - // Tell TypeScript how to load modules that start with with `@angular` - tsParser.options.paths = { '@angular/*': [API_SOURCE_PATH + '/*'] }; - tsParser.options.baseUrl = '.'; + // Tell TypeScript how to load modules that start with with `@angular` + tsParser.options.paths = {'@angular/*': [API_SOURCE_PATH + '/*']}; + tsParser.options.baseUrl = '.'; - // API files are typescript - readTypeScriptModules.basePath = API_SOURCE_PATH; - readTypeScriptModules.ignoreExportsMatching = [/^_|^ɵɵ|^VERSION$/]; - readTypeScriptModules.hidePrivateMembers = true; + // API files are typescript + readTypeScriptModules.basePath = API_SOURCE_PATH; + readTypeScriptModules.ignoreExportsMatching = [/^_|^ɵɵ|^VERSION$/]; + readTypeScriptModules.hidePrivateMembers = true; - // NOTE: This list should be in sync with tools/public_api_guard/BUILD.bazel - readTypeScriptModules.sourceFiles = [ - 'animations/index.ts', - 'animations/browser/index.ts', - 'animations/browser/testing/index.ts', - 'common/http/index.ts', - 'common/http/testing/index.ts', - 'common/index.ts', - 'common/testing/index.ts', - 'core/index.ts', - 'core/testing/index.ts', - 'elements/index.ts', - 'forms/index.ts', - // Current plan for Angular v8 is to hide documentation for the @angular/http package - // 'http/index.ts', - // 'http/testing/index.ts', - 'platform-browser/index.ts', - 'platform-browser/animations/index.ts', - 'platform-browser/testing/index.ts', - 'platform-browser-dynamic/index.ts', - 'platform-browser-dynamic/testing/index.ts', - 'platform-server/index.ts', - 'platform-server/testing/index.ts', - 'platform-webworker/index.ts', - 'platform-webworker-dynamic/index.ts', - 'router/index.ts', - 'router/testing/index.ts', - 'router/upgrade/index.ts', - 'service-worker/index.ts', - 'upgrade/index.ts', - 'upgrade/static/index.ts', - ]; + // NOTE: This list should be in sync with tools/public_api_guard/BUILD.bazel + readTypeScriptModules.sourceFiles = [ + 'animations/index.ts', + 'animations/browser/index.ts', + 'animations/browser/testing/index.ts', + 'common/http/index.ts', + 'common/http/testing/index.ts', + 'common/index.ts', + 'common/testing/index.ts', + 'common/upgrade/index.ts', + 'core/index.ts', + 'core/testing/index.ts', + 'elements/index.ts', + 'forms/index.ts', + // Current plan for Angular v8 is to hide documentation for the @angular/http package + // 'http/index.ts', + // 'http/testing/index.ts', + 'platform-browser/index.ts', + 'platform-browser/animations/index.ts', + 'platform-browser/testing/index.ts', + 'platform-browser-dynamic/index.ts', + 'platform-browser-dynamic/testing/index.ts', + 'platform-server/index.ts', + 'platform-server/testing/index.ts', + 'platform-webworker/index.ts', + 'platform-webworker-dynamic/index.ts', + 'router/index.ts', + 'router/testing/index.ts', + 'router/upgrade/index.ts', + 'service-worker/index.ts', + 'upgrade/index.ts', + 'upgrade/static/index.ts', + ]; - readFilesProcessor.fileReaders.push(packageContentFileReader); + readFilesProcessor.fileReaders.push(packageContentFileReader); - // API Examples - readFilesProcessor.sourceFiles = [ - { - basePath: API_SOURCE_PATH, - include: API_SOURCE_PATH + '/examples/**/*', - fileReader: 'exampleFileReader' - }, - { - basePath: API_SOURCE_PATH, - include: API_SOURCE_PATH + '/**/PACKAGE.md', - fileReader: 'packageContentFileReader' - } - ]; - collectExamples.exampleFolders.push('examples'); - }) + // API Examples + readFilesProcessor.sourceFiles = [ + { + basePath: API_SOURCE_PATH, + include: API_SOURCE_PATH + '/examples/**/*', + fileReader: 'exampleFileReader' + }, + { + basePath: API_SOURCE_PATH, + include: API_SOURCE_PATH + '/**/PACKAGE.md', + fileReader: 'packageContentFileReader' + } + ]; + collectExamples.exampleFolders.push('examples'); + }) - // Configure jsdoc-style tag parsing - .config(function(parseTagsProcessor, getInjectables, tsHost) { - // Load up all the tag definitions in the tag-defs folder - parseTagsProcessor.tagDefinitions = - parseTagsProcessor.tagDefinitions.concat(getInjectables(requireFolder(__dirname, './tag-defs'))); - // We don't want license headers to be joined to the first API item's comment - tsHost.concatMultipleLeadingComments = false; - }) + // Configure jsdoc-style tag parsing + .config(function(parseTagsProcessor, getInjectables, tsHost) { + // Load up all the tag definitions in the tag-defs folder + parseTagsProcessor.tagDefinitions = parseTagsProcessor.tagDefinitions.concat( + getInjectables(requireFolder(__dirname, './tag-defs'))); + // We don't want license headers to be joined to the first API item's comment + tsHost.concatMultipleLeadingComments = false; + }) - .config(function(computeStability, splitDescription, addNotYetDocumentedProperty, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES) { - computeStability.docTypes = API_DOC_TYPES_TO_RENDER; - // Only split the description on the API docs - splitDescription.docTypes = API_DOC_TYPES.concat(['package-content']); - addNotYetDocumentedProperty.docTypes = API_DOC_TYPES; - }) + .config(function( + computeStability, splitDescription, addNotYetDocumentedProperty, + API_DOC_TYPES_TO_RENDER, API_DOC_TYPES) { + computeStability.docTypes = API_DOC_TYPES_TO_RENDER; + // Only split the description on the API docs + splitDescription.docTypes = API_DOC_TYPES.concat(['package-content']); + addNotYetDocumentedProperty.docTypes = API_DOC_TYPES; + }) - .config(function(mergeDecoratorDocs) { - mergeDecoratorDocs.propertiesToMerge = [ - 'shortDescription', - 'description', - 'security', - 'deprecated', - 'see', - 'usageNotes', - ]; - }) + .config(function(mergeDecoratorDocs) { + mergeDecoratorDocs.propertiesToMerge = [ + 'shortDescription', + 'description', + 'security', + 'deprecated', + 'see', + 'usageNotes', + ]; + }) - .config(function(checkContentRules, API_DOC_TYPES, API_CONTAINED_DOC_TYPES) { - addMinLengthRules(checkContentRules); - addHeadingRules(checkContentRules, API_DOC_TYPES); - addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES); - checkContentRules.failOnContentErrors = true; - }) + .config(function(checkContentRules, API_DOC_TYPES, API_CONTAINED_DOC_TYPES) { + addMinLengthRules(checkContentRules); + addHeadingRules(checkContentRules, API_DOC_TYPES); + addAllowedPropertiesRules(checkContentRules, API_CONTAINED_DOC_TYPES); + checkContentRules.failOnContentErrors = true; + }) - .config(function(filterContainedDocs, API_CONTAINED_DOC_TYPES) { - filterContainedDocs.docTypes = API_CONTAINED_DOC_TYPES; - }) + .config(function(filterContainedDocs, API_CONTAINED_DOC_TYPES) { + filterContainedDocs.docTypes = API_CONTAINED_DOC_TYPES; + }) - .config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) { + .config(function(computePathsProcessor, EXPORT_DOC_TYPES, generateApiListDoc) { - const API_SEGMENT = 'api'; + const API_SEGMENT = 'api'; - generateApiListDoc.outputFolder = API_SEGMENT; + generateApiListDoc.outputFolder = API_SEGMENT; - computePathsProcessor.pathTemplates.push({ - docTypes: ['package'], - getPath: function computeModulePath(doc) { - doc.moduleFolder = `${API_SEGMENT}/${doc.id.replace(/\/index$/, '')}`; - return doc.moduleFolder; - }, - outputPathTemplate: '${moduleFolder}.json' - }); - computePathsProcessor.pathTemplates.push({ - docTypes: EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe']), - pathTemplate: '${moduleDoc.moduleFolder}/${name}', - outputPathTemplate: '${moduleDoc.moduleFolder}/${name}.json', - }); - }) + computePathsProcessor.pathTemplates.push({ + docTypes: ['package'], + getPath: function computeModulePath(doc) { + doc.moduleFolder = `${API_SEGMENT}/${doc.id.replace(/\/index$/, '')}`; + return doc.moduleFolder; + }, + outputPathTemplate: '${moduleFolder}.json' + }); + computePathsProcessor.pathTemplates.push({ + docTypes: EXPORT_DOC_TYPES.concat(['decorator', 'directive', 'ngmodule', 'pipe']), + pathTemplate: '${moduleDoc.moduleFolder}/${name}', + outputPathTemplate: '${moduleDoc.moduleFolder}/${name}.json', + }); + }) - .config(function(templateFinder) { - // Where to find the templates for the API doc rendering - templateFinder.templateFolders.unshift(API_TEMPLATES_PATH); - }) + .config(function(templateFinder) { + // Where to find the templates for the API doc rendering + templateFinder.templateFolders.unshift(API_TEMPLATES_PATH); + }) - .config(function(convertToJsonProcessor, postProcessHtml, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES, autoLinkCode) { - convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER); - postProcessHtml.docTypes = convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER); - autoLinkCode.docTypes = API_DOC_TYPES; - autoLinkCode.codeElements = ['code', 'code-example', 'code-pane']; - }); + .config(function( + convertToJsonProcessor, postProcessHtml, API_DOC_TYPES_TO_RENDER, API_DOC_TYPES, + autoLinkCode) { + convertToJsonProcessor.docTypes = + convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER); + postProcessHtml.docTypes = + convertToJsonProcessor.docTypes.concat(API_DOC_TYPES_TO_RENDER); + autoLinkCode.docTypes = API_DOC_TYPES; + autoLinkCode.codeElements = ['code', 'code-example', 'code-pane']; + }); function addMinLengthRules(checkContentRules) { diff --git a/aio/tools/transforms/angular-content-package/index.js b/aio/tools/transforms/angular-content-package/index.js index bdc4b700ec..df24e0e8ee 100644 --- a/aio/tools/transforms/angular-content-package/index.js +++ b/aio/tools/transforms/angular-content-package/index.js @@ -43,7 +43,7 @@ module.exports = new Package('angular-content', [basePackage, contentPackage]) readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([ { basePath: CONTENTS_PATH, - include: CONTENTS_PATH + '/{getting-started,guide,tutorial}/**/*.md', + include: CONTENTS_PATH + '/{start,guide,tutorial}/**/*.md', fileReader: 'contentFileReader' }, { diff --git a/aio/tools/transforms/authors-package/api-package.js b/aio/tools/transforms/authors-package/api-package.js index d079d126c7..aff578cf67 100644 --- a/aio/tools/transforms/authors-package/api-package.js +++ b/aio/tools/transforms/authors-package/api-package.js @@ -11,7 +11,7 @@ const { API_SOURCE_PATH } = require('../config'); const packageMap = { animations: ['animations/index.ts', 'animations/browser/index.ts', 'animations/browser/testing/index.ts'], - common: ['common/index.ts', 'common/testing/index.ts', 'common/http/index.ts', 'common/http/testing/index.ts'], + common: ['common/index.ts', 'common/testing/index.ts', 'common/upgrade/index.ts', 'common/http/index.ts', 'common/http/testing/index.ts'], core: ['core/index.ts', 'core/testing/index.ts'], elements: ['elements/index.ts'], forms: ['forms/index.ts'], diff --git a/aio/tools/transforms/authors-package/getting-started-package.js b/aio/tools/transforms/authors-package/getting-started-package.js index d7a1277d00..350cee340e 100644 --- a/aio/tools/transforms/authors-package/getting-started-package.js +++ b/aio/tools/transforms/authors-package/getting-started-package.js @@ -16,7 +16,7 @@ const { CONTENTS_PATH } = require('../config'); function createPackage(tutorialName) { - const tutorialFilePath = `${CONTENTS_PATH}/getting-started/${tutorialName}.md`; + const tutorialFilePath = `${CONTENTS_PATH}/start/${tutorialName}.md`; const tutorialFile = readFileSync(tutorialFilePath, 'utf8'); const examples = []; tutorialFile.replace(/]*path="([^"]+)"/g, (_, path) => examples.push('examples/' + path)); diff --git a/aio/tools/transforms/authors-package/index.js b/aio/tools/transforms/authors-package/index.js index b747bc8302..f8bad88e37 100644 --- a/aio/tools/transforms/authors-package/index.js +++ b/aio/tools/transforms/authors-package/index.js @@ -22,7 +22,7 @@ function createPackage(changedFile) { return require('./tutorial-package').createPackage(tutorialName); } - const gettingStartedMatch = /^aio\/content\/getting-started\/([^.]+)\.md/.exec(changedFile); + const gettingStartedMatch = /^aio\/content\/start\/([^.]+)\.md/.exec(changedFile); const gettingStartedExampleMatch = /^aio\/content\/examples\/getting-started\/([^\/]+)\//.exec(changedFile); if (gettingStartedMatch || gettingStartedExampleMatch) { const gettingStartedName = gettingStartedMatch && gettingStartedMatch[1] || 'index'; diff --git a/aio/yarn.lock b/aio/yarn.lock index 1bd5f6d27a..f4aeaec740 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -3247,6 +3247,16 @@ ecdsa-sig-formatter@1.0.9: base64url "^2.0.0" safe-buffer "^5.0.1" +ecstatic@^3.0.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.1.tgz#b15b5b036c2233defc78d7bacbd8765226c95577" + integrity sha512-/rrctvxZ78HMI/tPIsqdvFKHHscxR3IJuKrZI2ZoUgkt2SiufyLFBmcco+aqQBIu6P1qBsUNG3drAAGLx80vTQ== + dependencies: + he "^1.1.1" + mime "^1.6.0" + minimist "^1.1.0" + url-join "^2.0.5" + editions@^1.1.1, editions@^1.1.2, editions@^1.3.1, editions@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.3.tgz#0907101bdda20fac3cbe334c27cbd0688dc99a5b" @@ -4870,12 +4880,7 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - -he@1.2.x: +he@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -5049,12 +5054,19 @@ http-proxy@^1.17.0: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-server-spa@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/http-server-spa/-/http-server-spa-1.3.0.tgz#2892c0ade750e1c3826b3589744e1c17c46aa6a5" - integrity sha512-NfXBksDzoiBOo1IrMDtxpKJ8FOHLqy0YdijYjqMoRcS7AWPf6MzhRvKe2KiXxENlqTRqkOH418SvbxC6GzG2TA== +http-server@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b" + integrity sha512-6JeGDGoujJLmhjiRGlt8yK8Z9Kl0vnl/dQoQZlc4oeqaUoAKQg94NILLfrY3oWzSyFaQCVNTcKE5PZ3cH8VP9w== dependencies: - mime "^1.3.4" + colors "1.0.3" + corser "~2.0.0" + ecstatic "^3.0.0" + http-proxy "^1.8.1" + opener "~1.4.0" + optimist "0.6.x" + portfinder "^1.0.13" + union "~0.4.3" http-signature@~1.2.0: version "1.2.0" @@ -6963,7 +6975,7 @@ mime@1.4.1, mime@^1.3.4: resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== -mime@^1.4.1: +mime@^1.4.1, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -8139,11 +8151,6 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - portfinder@^1.0.13: version "1.0.19" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f" @@ -10917,6 +10924,11 @@ url-join@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" +url-join@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" + integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" diff --git a/browser_repositories.bzl b/browser_repositories.bzl new file mode 100644 index 0000000000..8c7b04462d --- /dev/null +++ b/browser_repositories.bzl @@ -0,0 +1,114 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pinned browser versions for karma testing +""" + +load("@io_bazel_rules_webtesting//web/internal:platform_http_file.bzl", "platform_http_file") + +def browser_repositories(): + """Load pinned rules_webtesting browser versions.""" + + platform_http_file( + name = "org_chromium_chromium", + amd64_sha256 = + "eb6754c7918da5eab42a42bbda7efdf7f1661eaa3802b8940841f0c2c312299f", + amd64_urls = [ + # Chromium 74.0.3729.0 (2019-03-08 snaphot 638880) + # https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/638880/ + # Current linux stable as of 2019-05-15 + # https://www.chromium.org/developers/calendar + "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Linux_x64/638880/chrome-linux.zip", + ], + licenses = ["notice"], # BSD 3-clause (maybe more?) + macos_sha256 = + "c48bdffac6a91c85c17a848012b1a45fbf36e3a2d4aaac5b6ded8ac65b1d96e3", + macos_urls = [ + # Chromium 74.0.3729.0 (2019-03-08 snaphot 638880) + # https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/638880/ + # Current mac stable as of 2019-05-15 + # https://www.chromium.org/developers/calendar + "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/638880/chrome-mac.zip", + ], + windows_sha256 = + "d1bb728118c12ea436d8ea07dba980789e7d860aa664dd1fad78bc20e8d9391c", + windows_urls = [ + # Chromium 66.0.3359.0 (2018-03-01 snaphot 540270) + # https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/612439/ + # NOTE: There is an issue with chromium 68-71 with Windows: https://bugs.chromium.org/p/chromium/issues/detail?id=540270 + # and pinning to 72 is not possible as the archive name has changed to chrome-win.zip which breaks + # as the executable path the hard-coded in rules_webtesting and includes the archive name. + "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Win_x64/540270/chrome-win32.zip", + ], + ) + + platform_http_file( + name = "org_chromium_chromedriver", + amd64_sha256 = + "ec9dbe021338f0befaecca702abc576cb7cc31a2f5a852c2c41e94721af5d3ad", + amd64_urls = [ + # ChromeDriver 74.0.3729.6 supports Chrome 74 + # http://chromedriver.chromium.org/downloads + "https://chromedriver.storage.googleapis.com/74.0.3729.6/chromedriver_linux64.zip", + ], + licenses = ["reciprocal"], # BSD 3-clause, ICU, MPL 1.1, libpng (BSD/MIT-like), Academic Free License v. 2.0, BSD 2-clause, MIT + macos_sha256 = + "b4b73681404d231d81a9b7ab9d4f0cb090f3e69240296eca2eb46e2629519152", + macos_urls = [ + # ChromeDriver 74.0.3729.6 supports Chrome 74 + # http://chromedriver.chromium.org/downloads + "https://chromedriver.storage.googleapis.com/74.0.3729.6/chromedriver_mac64.zip", + ], + windows_sha256 = + "a8fa028acebef7b931ef9cb093f02865f9f7495e49351f556e919f7be77f072e", + windows_urls = [ + # ChromeDriver 2.38 supports Chrome v65-67 + # http://chromedriver.chromium.org/downloads + "https://chromedriver.storage.googleapis.com/2.38/chromedriver_win32.zip", + ], + ) + + platform_http_file( + name = "org_mozilla_firefox", + amd64_sha256 = + "3a729ddcb1e0f5d63933177a35177ac6172f12edbf9fbbbf45305f49333608de", + amd64_urls = [ + "https://mirror.bazel.build/ftp.mozilla.org/pub/firefox/releases/61.0.2/linux-x86_64/en-US/firefox-61.0.2.tar.bz2", + "https://ftp.mozilla.org/pub/firefox/releases/61.0.2/linux-x86_64/en-US/firefox-61.0.2.tar.bz2", + ], + licenses = ["reciprocal"], # MPL 2.0 + macos_sha256 = + "bf23f659ae34832605dd0576affcca060d1077b7bf7395bc9874f62b84936dc5", + macos_urls = [ + "https://mirror.bazel.build/ftp.mozilla.org/pub/firefox/releases/61.0.2/mac/en-US/Firefox%2061.0.2.dmg", + "https://ftp.mozilla.org/pub/firefox/releases/61.0.2/mac/en-US/Firefox%2061.0.2.dmg", + ], + ) + + platform_http_file( + name = "org_mozilla_geckodriver", + amd64_sha256 = + "c9ae92348cf00aa719be6337a608fae8304691a95668e8e338d92623ba9e0ec6", + amd64_urls = [ + "https://mirror.bazel.build/github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz", + "https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz", + ], + licenses = ["reciprocal"], # MPL 2.0 + macos_sha256 = + "ce4a3e9d706db94e8760988de1ad562630412fa8cf898819572522be584f01ce", + macos_urls = [ + "https://mirror.bazel.build/github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-macos.tar.gz", + "https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-macos.tar.gz", + ], + ) diff --git a/docs/BAZEL.md b/docs/BAZEL.md index c381cf2d81..f993420276 100644 --- a/docs/BAZEL.md +++ b/docs/BAZEL.md @@ -81,6 +81,9 @@ See also: [`//.bazelrc`](https://github.com/angular/angular/blob/master/.bazelrc The process should automatically connect to the debugger. For additional info and testing options, see the [nodejs_test documentation](https://bazelbuild.github.io/rules_nodejs/node/node.html#nodejs_test). +- Click on "Resume script execution" to let the code run until the first `debugger` statement or a previously set breakpoint. +- If you're debugging an ivy test and you want to inspect the generated template instructions, find the template of your component in the call stack and click on `(source mapped from [CompName].js)` at the bottom of the code. You can also disable sourcemaps in the options or go to sources and look into ng:// namespace to see all the generated code. + ### Debugging a Node Test in VSCode First time setup: @@ -248,11 +251,6 @@ Usually there is a single item (or multiple items of the same kind) where the ov ## Known issues -### Webstorm - -The autocompletion in WebStorm can be added via a Bazel plugin intended for IntelliJ IDEA, but the plugin needs to be installed in a special way. -See [bazelbuild/intellij#246](https://github.com/bazelbuild/intellij/issues/246) for more info. - ### Xcode If you see the following error: diff --git a/docs/PUBLIC_API.md b/docs/PUBLIC_API.md index 53899d5770..40d1d08227 100644 --- a/docs/PUBLIC_API.md +++ b/docs/PUBLIC_API.md @@ -37,7 +37,7 @@ We explicitly don't consider the following to be our public API surface: - any file/import paths within our package except for the `/`, `/testing` and `/bundles/*` and other documented package entry-points. - constructors of injectable classes (services and directives) - please use DI to obtain instances of these classes -- any class members or symbols marked as `private`, or prefixed with underscore (`_`), [barred latin o](https://en.wikipedia.org/wiki/%C6%9F) (`ɵ`), and [double barred latin o](https://en.wikipedia.org/wiki/%C6%9F) (`ɵɵ`). +- any class members or symbols marked as `private`, or prefixed with underscore (`_`), [barred latin o](https://en.wikipedia.org/wiki/%C6%9F) (`ɵ`), and double barred latin o (`ɵɵ`). - extending any of our classes unless the support for this is specifically documented in the API docs - the contents and API surface of the code generated by Angular's compiler (with one notable exception: the existence and name of `NgModuleFactory` instances exported from generated code is guaranteed) diff --git a/docs/TRIAGE_AND_LABELS.md b/docs/TRIAGE_AND_LABELS.md index b5a307110a..4622f788eb 100644 --- a/docs/TRIAGE_AND_LABELS.md +++ b/docs/TRIAGE_AND_LABELS.md @@ -120,17 +120,30 @@ Triaging PRs is the same as triaging issues, except that the labels `frequency: PRs also have additional label categories that should be used to signal their state. -Every triaged PR must have a `pr_action` label assigned to it: +Every triaged PR must have a `PR action` label assigned to it: -* `PR action: cleanup` - more work is needed from the author. -* `PR action: discuss` - discussion is needed, to be led by the author. -* `PR action: merge` - the PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready". +* `PR action: discuss`: Discussion is needed, to be led by the author. + * _**Who adds it:** Typically the PR author._ + * _**Who removes it:** Whoever added it._ +* `PR action: review` (optional): One or more reviews are pending. The label is optional, since the review status can be derived from GitHub's Reviewers interface. + * _**Who adds it:** Any team member. The caretaker can use it to differentiate PRs pending review from merge-ready PRs._ + * _**Who removes it:** Whoever added it or the reviewer adding the last missing review._ +* `PR action: cleanup`: More work is needed from the author. + * _**Who adds it:** The reviewer requesting changes to the PR._ + * _**Who removes it:** Either the author (after implementing the requested changes) or the reviewer (after confirming the requested changes have been implemented)._ +* `PR action: merge`: The PR author is ready for the changes to be merged by the caretaker as soon as the PR is green (or merge-assistance label is applied and caretaker has deemed it acceptable manually). In other words, this label indicates to "auto submit when ready". + * _**Who adds it:** Typically the PR author._ + * _**Who removes it:** Whoever added it._ In addition, PRs can have the following states: -* `PR state: WIP` - PR is experimental or rapidly changing. Not ready for review or triage. -* `PR state: blocked` - PR is blocked on an issue or other PR. Not ready for review or triage or merge. +* `PR state: WIP`: PR is experimental or rapidly changing. Not ready for review or triage. + * _**Who adds it:** The PR author._ + * _**Who removes it:** Whoever added it._ +* `PR state: blocked`: PR is blocked on an issue or other PR. Not ready for merge. + * _**Who adds it:** Any team member._ + * _**Who removes it:** Any team member._ When a PR is ready for review, a review should be requested using the Reviewers interface in Github. @@ -167,16 +180,31 @@ Only the `PR action: merge` label means that the PR is ready for merging. ## Special Labels ### `cla: yes`, `cla: no` +* _**Who adds it:** @googlebot, or a Googler manually overriding the status in case the bot got it wrong._ +* _**Who removes it:** @googlebot._ + Managed by googlebot. Indicates whether a PR has a CLA on file for its author(s). Only issues with `cla:yes` should be merged into master. ### `aio: preview` +* _**Who adds it:** Any team member. (Typically the author or a reviewer.)_ +* _**Who removes it:** Any team member. (Typically, whoever added it.)_ + Applying this label to a PR makes the angular.io preview available regardless of the author. [More info](../aio/aio-builds-setup/docs/overview--security-model.md) ### `PR action: merge-assistance` +* _**Who adds it:** Any team member._ +* _**Who removes it:** Any team member._ + This label can be added to let the caretaker know that the PR needs special attention. There should always be a comment added to the PR to explain why the caretaker's assistance is needed. The comment should be formatted like this: `merge-assistance: ` For example, the PR owner might not be a Googler and needs help to run g3sync; or one of the checks is failing due to external causes and the PR should still be merged. + +### `PR action: rerun CI at HEAD` +* _**Who adds it:** Any team member._ +* _**Who removes it:** The Angular Bot, once it triggers the CI rerun._ + +This label can be added to instruct the Angular Bot to rerun the CI jobs for the PR at latest HEAD of the branch it targets. diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index edcb9b1800..35c68b565d 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime": 1497, - "main": 164945, + "main": 166799, "polyfills": 43626 } } @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 14487, + "main": 14664, "polyfills": 43567 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 146225, + "main": 149248, "polyfills": 43567 } } diff --git a/integration/bazel/WORKSPACE b/integration/bazel/WORKSPACE index 53f174a705..e3007a6deb 100644 --- a/integration/bazel/WORKSPACE +++ b/integration/bazel/WORKSPACE @@ -5,8 +5,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # Fetch rules_nodejs so we can install our npm dependencies http_archive( name = "build_bazel_rules_nodejs", - sha256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf", - urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.27.12/rules_nodejs-0.27.12.tar.gz"], + sha256 = "395b7568f20822c13fc5abc65b1eced637446389181fda3a108fdd6ff2cac1e9", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.29.2/rules_nodejs-0.29.2.tar.gz"], ) # Fetch sass rules for compiling sass files diff --git a/integration/bazel/src/package.json b/integration/bazel/src/package.json index fcbd777e1a..4da69c3a85 100644 --- a/integration/bazel/src/package.json +++ b/integration/bazel/src/package.json @@ -19,8 +19,8 @@ "@angular/bazel": "packages-dist:bazel", "@angular/compiler": "packages-dist:compiler", "@angular/compiler-cli": "packages-dist:compiler-cli", - "@bazel/karma": "0.27.12", - "@bazel/typescript": "0.27.12", + "@bazel/karma": "0.29.0", + "@bazel/typescript": "0.29.0", "@types/jasmine": "2.8.8", "@types/source-map": "0.5.1", "protractor": "5.1.2", diff --git a/integration/dynamic-compiler/src/app.component.ts b/integration/dynamic-compiler/src/app.component.ts index e54316da9b..16ec786eb7 100644 --- a/integration/dynamic-compiler/src/app.component.ts +++ b/integration/dynamic-compiler/src/app.component.ts @@ -10,18 +10,16 @@ declare var System: any; `, }) export class AppComponent implements AfterViewInit { - @ViewChild('vc', {read: ViewContainerRef}) container: ViewContainerRef; + @ViewChild('vc', {read: ViewContainerRef, static: false}) container: ViewContainerRef; - constructor(private compiler: Compiler) { - } + constructor(private compiler: Compiler) {} ngAfterViewInit() { System.import('./dist/lazy.bundle.js').then((module: any) => { - this.compiler.compileModuleAndAllComponentsAsync(module.LazyModule) - .then((compiled) => { - const factory = compiled.componentFactories[0]; - this.container.createComponent(factory); - }); + this.compiler.compileModuleAndAllComponentsAsync(module.LazyModule).then((compiled) => { + const factory = compiled.componentFactories[0]; + this.container.createComponent(factory); + }); }); } } diff --git a/integration/side-effects/.gitignore b/integration/side-effects/.gitignore new file mode 100644 index 0000000000..0ec6ec881c --- /dev/null +++ b/integration/side-effects/.gitignore @@ -0,0 +1,3 @@ +# The check-side-effects package generates and deletes this file. +# If the process is killed, it will be left behind. +check-side-effects.tmp-input.js diff --git a/integration/side-effects/README.md b/integration/side-effects/README.md new file mode 100644 index 0000000000..b9a8146eaa --- /dev/null +++ b/integration/side-effects/README.md @@ -0,0 +1,9 @@ +This test checks if the side effects for loading Angular packages have changed using . + +Running `yarn test` will check all ES modules listed in `side-effects.json`. + +Running `yarn update` will update any changed side effects. + +To add a new ES module to this test, add a new entry in `side-effects.json`. + +Usually the ESM and FESM should have the same output, but retained objects that were renamed during the flattening step will leave behind a different name. diff --git a/integration/side-effects/package.json b/integration/side-effects/package.json new file mode 100644 index 0000000000..d4d211ee37 --- /dev/null +++ b/integration/side-effects/package.json @@ -0,0 +1,19 @@ +{ + "name": "angular-side-effects", + "version": "0.0.0", + "license": "MIT", + "scripts": { + "test": "check-side-effects --test side-effects.json --pure-getters", + "update": "yarn test --update" + }, + "dependencies": { + "@angular/animations": "file:../../dist/packages-dist/animations", + "@angular/common": "file:../../dist/packages-dist/common", + "@angular/core": "file:../../dist/packages-dist/core", + "@angular/elements": "file:../../dist/packages-dist/elements", + "@angular/forms": "file:../../dist/packages-dist/forms", + "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", + "@angular/router": "file:../../dist/packages-dist/router", + "check-side-effects": "file:../../node_modules/check-side-effects" + } +} diff --git a/integration/side-effects/side-effects.json b/integration/side-effects/side-effects.json new file mode 100644 index 0000000000..8ea44701f2 --- /dev/null +++ b/integration/side-effects/side-effects.json @@ -0,0 +1,132 @@ +{ + "tests": [ + { + "esModules": "./node_modules/@angular/animations/esm5/animations.js", + "expectedOutput": "./snapshots/animations/esm5.js" + }, + { + "esModules": "./node_modules/@angular/animations/fesm5/animations.js", + "expectedOutput": "./snapshots/animations/esm5.js" + }, + { + "esModules": "./node_modules/@angular/animations/esm2015/animations.js", + "expectedOutput": "./snapshots/animations/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/animations/fesm2015/animations.js", + "expectedOutput": "./snapshots/animations/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/animations/esm5/browser/browser.js", + "expectedOutput": "./snapshots/animations-browser/esm5.js" + }, + { + "esModules": "./node_modules/@angular/animations/fesm5/browser.js", + "expectedOutput": "./snapshots/animations-browser/esm5.js" + }, + { + "esModules": "./node_modules/@angular/animations/esm2015/browser/browser.js", + "expectedOutput": "./snapshots/animations-browser/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/animations/fesm2015/browser.js", + "expectedOutput": "./snapshots/animations-browser/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/common/esm5/common.js", + "expectedOutput": "./snapshots/common/esm5.js" + }, + { + "esModules": "./node_modules/@angular/common/fesm5/common.js", + "expectedOutput": "./snapshots/common/esm5.js" + }, + { + "esModules": "./node_modules/@angular/common/esm2015/common.js", + "expectedOutput": "./snapshots/common/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/common/fesm2015/common.js", + "expectedOutput": "./snapshots/common/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/core/esm5/core.js", + "expectedOutput": "./snapshots/core/esm5.js" + }, + { + "esModules": "./node_modules/@angular/core/fesm5/core.js", + "expectedOutput": "./snapshots/core/esm5.js" + }, + { + "esModules": "./node_modules/@angular/core/esm2015/core.js", + "expectedOutput": "./snapshots/core/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/core/fesm2015/core.js", + "expectedOutput": "./snapshots/core/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/elements/esm5/elements.js", + "expectedOutput": "./snapshots/elements/esm5.js" + }, + { + "esModules": "./node_modules/@angular/elements/fesm5/elements.js", + "expectedOutput": "./snapshots/elements/esm5.js" + }, + { + "esModules": "./node_modules/@angular/elements/esm2015/elements.js", + "expectedOutput": "./snapshots/elements/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/elements/fesm2015/elements.js", + "expectedOutput": "./snapshots/elements/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/forms/esm5/forms.js", + "expectedOutput": "./snapshots/forms/esm5.js" + }, + { + "esModules": "./node_modules/@angular/forms/fesm5/forms.js", + "expectedOutput": "./snapshots/forms/esm5.js" + }, + { + "esModules": "./node_modules/@angular/forms/esm2015/forms.js", + "expectedOutput": "./snapshots/forms/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/forms/fesm2015/forms.js", + "expectedOutput": "./snapshots/forms/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/platform-browser/esm5/platform-browser.js", + "expectedOutput": "./snapshots/platform-browser/esm5.js" + }, + { + "esModules": "./node_modules/@angular/platform-browser/fesm5/platform-browser.js", + "expectedOutput": "./snapshots/platform-browser/esm5.js" + }, + { + "esModules": "./node_modules/@angular/platform-browser/esm2015/platform-browser.js", + "expectedOutput": "./snapshots/platform-browser/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/platform-browser/fesm2015/platform-browser.js", + "expectedOutput": "./snapshots/platform-browser/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/router/esm5/router.js", + "expectedOutput": "./snapshots/router/esm5.js" + }, + { + "esModules": "./node_modules/@angular/router/fesm5/router.js", + "expectedOutput": "./snapshots/router/esm5.js" + }, + { + "esModules": "./node_modules/@angular/router/esm2015/router.js", + "expectedOutput": "./snapshots/router/esm2015.js" + }, + { + "esModules": "./node_modules/@angular/router/fesm2015/router.js", + "expectedOutput": "./snapshots/router/esm2015.js" + } + ] +} diff --git a/integration/side-effects/snapshots/animations-browser/esm2015.js b/integration/side-effects/snapshots/animations-browser/esm2015.js new file mode 100644 index 0000000000..a23be397a1 --- /dev/null +++ b/integration/side-effects/snapshots/animations-browser/esm2015.js @@ -0,0 +1,3 @@ +import "@angular/animations"; + +import "@angular/core"; diff --git a/integration/side-effects/snapshots/animations-browser/esm5.js b/integration/side-effects/snapshots/animations-browser/esm5.js new file mode 100644 index 0000000000..58aec1ffc5 --- /dev/null +++ b/integration/side-effects/snapshots/animations-browser/esm5.js @@ -0,0 +1,5 @@ +import "tslib"; + +import "@angular/animations"; + +import "@angular/core"; diff --git a/integration/side-effects/snapshots/animations/esm2015.js b/integration/side-effects/snapshots/animations/esm2015.js new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integration/side-effects/snapshots/animations/esm2015.js @@ -0,0 +1 @@ + diff --git a/integration/side-effects/snapshots/animations/esm5.js b/integration/side-effects/snapshots/animations/esm5.js new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integration/side-effects/snapshots/animations/esm5.js @@ -0,0 +1 @@ + diff --git a/integration/side-effects/snapshots/common/esm2015.js b/integration/side-effects/snapshots/common/esm2015.js new file mode 100644 index 0000000000..595d8ae0af --- /dev/null +++ b/integration/side-effects/snapshots/common/esm2015.js @@ -0,0 +1 @@ +import "@angular/core"; diff --git a/integration/side-effects/snapshots/common/esm5.js b/integration/side-effects/snapshots/common/esm5.js new file mode 100644 index 0000000000..783048bfcf --- /dev/null +++ b/integration/side-effects/snapshots/common/esm5.js @@ -0,0 +1,3 @@ +import "@angular/core"; + +import "tslib"; diff --git a/integration/side-effects/snapshots/core/esm2015.js b/integration/side-effects/snapshots/core/esm2015.js new file mode 100644 index 0000000000..5f8109856a --- /dev/null +++ b/integration/side-effects/snapshots/core/esm2015.js @@ -0,0 +1,3 @@ +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/core/esm5.js b/integration/side-effects/snapshots/core/esm5.js new file mode 100644 index 0000000000..09cc6db200 --- /dev/null +++ b/integration/side-effects/snapshots/core/esm5.js @@ -0,0 +1,5 @@ +import "tslib"; + +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/elements/esm2015.js b/integration/side-effects/snapshots/elements/esm2015.js new file mode 100644 index 0000000000..38f33e40ed --- /dev/null +++ b/integration/side-effects/snapshots/elements/esm2015.js @@ -0,0 +1,5 @@ +import "@angular/core"; + +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/elements/esm5.js b/integration/side-effects/snapshots/elements/esm5.js new file mode 100644 index 0000000000..4500888aa3 --- /dev/null +++ b/integration/side-effects/snapshots/elements/esm5.js @@ -0,0 +1,7 @@ +import "tslib"; + +import "@angular/core"; + +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/forms/esm2015.js b/integration/side-effects/snapshots/forms/esm2015.js new file mode 100644 index 0000000000..7b26ecaa69 --- /dev/null +++ b/integration/side-effects/snapshots/forms/esm2015.js @@ -0,0 +1,7 @@ +import "@angular/core"; + +import "@angular/platform-browser"; + +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/forms/esm5.js b/integration/side-effects/snapshots/forms/esm5.js new file mode 100644 index 0000000000..70eeb745ff --- /dev/null +++ b/integration/side-effects/snapshots/forms/esm5.js @@ -0,0 +1,9 @@ +import "tslib"; + +import "@angular/core"; + +import "@angular/platform-browser"; + +import "rxjs"; + +import "rxjs/operators"; diff --git a/integration/side-effects/snapshots/platform-browser/esm2015.js b/integration/side-effects/snapshots/platform-browser/esm2015.js new file mode 100644 index 0000000000..aa3513a190 --- /dev/null +++ b/integration/side-effects/snapshots/platform-browser/esm2015.js @@ -0,0 +1,3 @@ +import "@angular/common"; + +import "@angular/core"; diff --git a/integration/side-effects/snapshots/platform-browser/esm5.js b/integration/side-effects/snapshots/platform-browser/esm5.js new file mode 100644 index 0000000000..013438f340 --- /dev/null +++ b/integration/side-effects/snapshots/platform-browser/esm5.js @@ -0,0 +1,5 @@ +import "tslib"; + +import "@angular/common"; + +import "@angular/core"; diff --git a/integration/side-effects/snapshots/router/esm2015.js b/integration/side-effects/snapshots/router/esm2015.js new file mode 100644 index 0000000000..0f9f04eb6f --- /dev/null +++ b/integration/side-effects/snapshots/router/esm2015.js @@ -0,0 +1,9 @@ +import "@angular/common"; + +import "@angular/core"; + +import "rxjs"; + +import "rxjs/operators"; + +import "@angular/platform-browser"; diff --git a/integration/side-effects/snapshots/router/esm5.js b/integration/side-effects/snapshots/router/esm5.js new file mode 100644 index 0000000000..22e1dbe38a --- /dev/null +++ b/integration/side-effects/snapshots/router/esm5.js @@ -0,0 +1,11 @@ +import "tslib"; + +import "@angular/common"; + +import "@angular/core"; + +import "rxjs"; + +import "rxjs/operators"; + +import "@angular/platform-browser"; diff --git a/integration/side-effects/yarn.lock b/integration/side-effects/yarn.lock new file mode 100644 index 0000000000..a5c82011f4 --- /dev/null +++ b/integration/side-effects/yarn.lock @@ -0,0 +1,375 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@angular-devkit/build-optimizer@0.14.0-beta.5": + version "0.14.0-beta.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.14.0-beta.5.tgz#f842a0b2717517cdc8e40704076d6182feccb81a" + integrity sha512-sQ86BGrd65QD9fV+wgDWNFKS2kxsZFj/lSn3pjgguV43XjGvnNlXnsVAgZOruygyXjB/afEOkNpO/4sKFNxiMw== + dependencies: + loader-utils "1.2.3" + source-map "0.5.6" + typescript "3.2.4" + webpack-sources "1.3.0" + +"@angular/animations@file:../../dist/packages-dist/animations": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/common@file:../../dist/packages-dist/common": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/core@file:../../dist/packages-dist/core": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/elements@file:../../dist/packages-dist/elements": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/forms@file:../../dist/packages-dist/forms": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/platform-browser@file:../../dist/packages-dist/platform-browser": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@angular/router@file:../../dist/packages-dist/router": + version "8.0.0-rc.0" + dependencies: + tslib "^1.9.0" + +"@babel/code-frame@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" + integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/node@*": + version "11.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.1.tgz#9ee55ffce20f72e141863b0036a6e51c6fc09a1f" + integrity sha512-2azXFP9n4aA2QNLkKm/F9pzKxgYj1SMawZ5Eh9iC21RH3XNcFsivLVU2NhpMgQm7YobSByvIol4c42ZFusXFHQ== + +"@types/node@^11.13.9": + version "11.13.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.10.tgz#4df59e5966b56f512bac98898bcbee5067411f0f" + integrity sha512-leUNzbFTMX94TWaIKz8N15Chu55F9QSH+INKayQr5xpkasBQBRF3qQXfo3/dOnMU/dEIit+Y/SU8HyOjq++GwA== + +"@types/resolve@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" + integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== + dependencies: + "@types/node" "*" + +acorn@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +builtin-modules@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" + integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +"check-side-effects@file:../../node_modules/check-side-effects": + version "0.0.20" + dependencies: + "@angular-devkit/build-optimizer" "0.14.0-beta.5" + minimist "~1.2.0" + rollup "~1.11.3" + rollup-plugin-node-resolve "~4.2.3" + rollup-plugin-terser "~4.0.4" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +jest-worker@^24.0.0: + version "24.4.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.4.0.tgz#fbc452b0120bb5c2a70cdc88fa132b48eeb11dd0" + integrity sha512-BH9X/klG9vxwoO99ZBUbZFfV8qO0XNZ5SIiCyYK2zOuJBl6YJVAeNIQjcoOVNu4HGEHeYEKsUWws8kSlSbZ9YQ== + dependencies: + "@types/node" "*" + merge-stream "^1.0.1" + supports-color "^6.1.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +loader-utils@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + dependencies: + readable-stream "^2.0.1" + +minimist@^1.2.0, minimist@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +readable-stream@^2.0.1: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +resolve@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== + dependencies: + path-parse "^1.0.6" + +rollup-plugin-node-resolve@~4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.2.3.tgz#638a373a54287d19fcc088fdd1c6fd8a58e4d90a" + integrity sha512-r+WaesPzdGEynpLZLALFEDugA4ACa5zn7bc/+LVX4vAXQQ8IgDHv0xfsSvJ8tDXUtprfBtrDtRFg27ifKjcJTg== + dependencies: + "@types/resolve" "0.0.8" + builtin-modules "^3.1.0" + is-module "^1.0.0" + resolve "^1.10.0" + +rollup-plugin-terser@~4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz#6f661ef284fa7c27963d242601691dc3d23f994e" + integrity sha512-wPANT5XKVJJ8RDUN0+wIr7UPd0lIXBo4UdJ59VmlPCtlFsE20AM+14pe+tk7YunCsWEiuzkDBY3QIkSCjtrPXg== + dependencies: + "@babel/code-frame" "^7.0.0" + jest-worker "^24.0.0" + serialize-javascript "^1.6.1" + terser "^3.14.1" + +rollup@~1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.11.3.tgz#6f436db2a2d6b63f808bf60ad01a177643dedb81" + integrity sha512-81MR7alHcFKxgWzGfG7jSdv+JQxSOIOD/Fa3iNUmpzbd7p+V19e1l9uffqT8/7YAHgGOzmoPGN3Fx3L2ptOf5g== + dependencies: + "@types/estree" "0.0.39" + "@types/node" "^11.13.9" + acorn "^6.1.1" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +serialize-javascript@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" + integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-support@~0.5.10: + version "0.5.11" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2" + integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + +source-map@^0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +terser@^3.14.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== + dependencies: + commander "^2.19.0" + source-map "~0.6.1" + source-map-support "~0.5.10" + +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +typescript@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d" + integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +webpack-sources@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" diff --git a/modules/benchmarks/src/expanding_rows/BUILD.bazel b/modules/benchmarks/src/expanding_rows/BUILD.bazel new file mode 100644 index 0000000000..0592ca38ca --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/BUILD.bazel @@ -0,0 +1,74 @@ +package(default_visibility = ["//modules/benchmarks:__subpackages__"]) + +load("//tools:defaults.bzl", "ts_library") +load("//tools:defaults.bzl", "ng_module", "ng_rollup_bundle") +load("@npm_bazel_typescript//:index.bzl", "ts_devserver") +load("//modules/benchmarks:benchmark_test.bzl", "benchmark_test") + +ng_module( + name = "application_lib", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//packages:types", + "//packages/common", + "//packages/core", + "//packages/platform-browser", + "@npm//rxjs", + ], +) + +ts_library( + name = "perf_lib", + testonly = 1, + srcs = ["benchmark_perf.spec.ts"], + deps = [ + "//modules/e2e_util", + "@npm//protractor", + ], +) + +ng_rollup_bundle( + name = "bundle", + entry_point = "modules/benchmarks/src/expanding_rows/index.js", + deps = [ + ":application_lib", + "@npm//rxjs", + ], +) + +ts_devserver( + name = "prodserver", + static_files = [ + ":bundle.min_debug.js", + ":bundle.min.js", + "@npm//node_modules/zone.js:dist/zone.js", + "index.html", + ], +) + +ts_devserver( + name = "devserver", + entry_module = "angular/modules/benchmarks/src/expanding_rows/index", + index_html = "index.html", + scripts = [ + "@npm//node_modules/tslib:tslib.js", + "//tools/rxjs:rxjs_umd_modules", + ], + serving_path = "/index.js", + static_files = [ + "@npm//node_modules/zone.js:dist/zone.js", + "index.html", + ], + deps = [":application_lib"], +) + +benchmark_test( + name = "perf", + server = ":prodserver", + deps = [ + ":perf_lib", + ], +) diff --git a/modules/benchmarks/src/expanding_rows/benchmark.ts b/modules/benchmarks/src/expanding_rows/benchmark.ts new file mode 100644 index 0000000000..2fc9492f0a --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/benchmark.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright Google Inc. 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 {CommonModule} from '@angular/common'; +import {AfterViewInit, Component, NgModule, ViewChild, ViewEncapsulation} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; + +import {BenchmarkModule} from './benchmark_module'; +import {BenchmarkableExpandingRow} from './benchmarkable_expanding_row'; +import {BenchmarkableExpandingRowModule} from './benchmarkable_expanding_row_module'; + +@Component({ + selector: 'benchmark-root', + encapsulation: ViewEncapsulation.None, + template: ` +

cfc-expanding-row initialization benchmark

+ +
+ + + +
+ + + + `, +}) +export class InitializationRoot implements AfterViewInit { + @ViewChild(BenchmarkableExpandingRow, {static: true}) + expandingRow !: BenchmarkableExpandingRow; + + ngAfterViewInit() {} + + reset() { this.expandingRow.reset(); } + + init() { this.expandingRow.init(); } + + async runAll() { + await execTimed('initialization_benchmark', async() => { await this.doInit(); }); + } + + async handleInitClick() { await this.doInit(); } + + private async doInit() { + await execTimed('initial_load', async() => { this.expandingRow.init(); }); + } +} + +@NgModule({ + declarations: [InitializationRoot], + exports: [InitializationRoot], + imports: [ + CommonModule, + BenchmarkableExpandingRowModule, + BenchmarkModule, + BrowserModule, + ], + bootstrap: [InitializationRoot], +}) +// Component benchmarks must export a BenchmarkModule. +export class ExpandingRowBenchmarkModule { +} + +export async function execTimed(description: string, func: () => Promise) { + console.time(description); + await func(); + await nextTick(200); + console.timeEnd(description); +} + +export async function nextTick(delay = 1) { + return new Promise((res, rej) => { setTimeout(() => { res(); }, delay); }); +} diff --git a/modules/benchmarks/src/expanding_rows/benchmark_module.ts b/modules/benchmarks/src/expanding_rows/benchmark_module.ts new file mode 100644 index 0000000000..6f13981f60 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/benchmark_module.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright Google Inc. 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 {Component, ErrorHandler, Injectable, NgModule} from '@angular/core'; + +@Component({ + selector: 'benchmark-area', + template: '', + styles: [` + :host { + padding: 1; + margin: 1; + background-color: white; + width: 1000px; + display: block; + }`], + host: { + 'class': 'cfc-ng2-region', + } +}) +export class BenchmarkArea { +} + +declare interface ExtendedWindow extends Window { benchmarkErrors?: string[]; } +const extendedWindow = window as ExtendedWindow; + +@Injectable({providedIn: 'root'}) +export class BenchmarkErrorHandler implements ErrorHandler { + handleError(error: Error) { + if (!extendedWindow.benchmarkErrors) { + extendedWindow.benchmarkErrors = []; + } + extendedWindow.benchmarkErrors.push(error.message); + console.error(error); + } +} + +@NgModule({ + declarations: [BenchmarkArea], + exports: [BenchmarkArea], + providers: [ + {provide: ErrorHandler, useClass: BenchmarkErrorHandler}, + ] +}) +export class BenchmarkModule { +} diff --git a/modules/benchmarks/src/expanding_rows/benchmark_perf.spec.ts b/modules/benchmarks/src/expanding_rows/benchmark_perf.spec.ts new file mode 100644 index 0000000000..d8aec894e3 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/benchmark_perf.spec.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. 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 {$, browser} from 'protractor'; +import {runBenchmark} from '../../../e2e_util/perf_util'; + +describe('benchmarks', () => { + + it('should work for create', done => { + browser.rootEl = '#root'; + runBenchmark({ + id: 'create', + url: '', + ignoreBrowserSynchronization: true, + params: [], + prepare: () => $('#reset').click(), + work: () => $('#init').click() + }).then(done, done.fail); + }); + +}); diff --git a/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row.ts b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row.ts new file mode 100644 index 0000000000..8c70934388 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google Inc. 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 {Component} from '@angular/core'; + +export interface MlbTeam { + name: string; + id: number; + division: string; + stadium: string; + projection: string; +} + +@Component({ + selector: 'benchmarkable-expanding-row', + template: ` + + + + Team {{team.id}} + + + {{team.name}} + + {{team.id}} + + + +
+ + + `, +}) +export class BenchmarkableExpandingRow { + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + showExpandingRow !: boolean; + + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + teams !: MlbTeam[]; + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + private fakeTeams !: MlbTeam[]; + + init(): void { + this.teams = this.fakeTeams; + this.showExpandingRow = true; + } + + reset(numItems = 5000): void { + this.showExpandingRow = false; + + this.fakeTeams = []; + for (let i = 0; i < numItems; i++) { + this.fakeTeams.push({ + name: `name ${i}`, + id: i, + division: `division ${i}`, + stadium: `stadium ${i}`, + projection: `projection ${i}`, + }); + } + } +} diff --git a/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts new file mode 100644 index 0000000000..7459cf92f4 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/benchmarkable_expanding_row_module.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Google Inc. 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 {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; + +import {BenchmarkableExpandingRow} from './benchmarkable_expanding_row'; +import {ExpandingRowModule} from './expanding_row_module'; + +@NgModule({ + declarations: [BenchmarkableExpandingRow], + exports: [BenchmarkableExpandingRow], + imports: [ + CommonModule, + ExpandingRowModule, + ], +}) +export class BenchmarkableExpandingRowModule { +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row.ts b/modules/benchmarks/src/expanding_rows/expanding_row.ts new file mode 100644 index 0000000000..da6da38bb8 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row.ts @@ -0,0 +1,355 @@ +/** + * @license + * Copyright Google Inc. 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 {AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, InjectionToken, Input, Output, QueryList, ViewChild} from '@angular/core'; + +import {expanding_row_css} from './expanding_row_css'; +import {ExpandingRowSummary} from './expanding_row_summary'; +import {ExpandingRowToggleEvent} from './expanding_row_toggle_event'; + + +/** + * Injection token to break cylic dependency between ExpandingRow and + * ExpandingRowHost + */ +export const EXPANDING_ROW_HOST_INJECTION_TOKEN = + new InjectionToken('ExpandingRowHost'); + +/** The base class for ExpandingRowHost component to break cylic dependency. */ +export interface ExpandingRowHostBase { + /** + * A reference to all child cfc-expanding-row elements. We will need for + * keyboard accessibility and scroll adjustments. For example, we need to know + * which row is previous row when user presses "left arrow" on a focused row. + */ + contentRows: QueryList; + + /** + * Keeps track of the last row that had focus before focus left the list + * of expanding rows. + */ + lastFocusedRow?: ExpandingRow; + + /** + * Handles summary element click on a cfc-expanding-row component. Note + * that summary element is visible only when the row is collapsed. So this + * event will fired prior to expansion of a collapsed row. Scroll adjustment + * below makes sure mouse stays on the caption element when the collapsed + * row expands. + */ + handleRowSummaryClick(row: ExpandingRow): void; + + /** + * Check if element is blacklisted. Blacklisted elements will not collapse an + * open row when clicked. + */ + isBlacklisted(element: HTMLElement|null): boolean; + + /** + * Handles caption element click on a cfc-expanding-row component. Note + * that caption element is visible only when the row is expanded. So this + * means we will collapse the expanded row. The scroll adjustment below + * makes sure that the mouse stays under the summary of the expanded row + * when the row collapses. + */ + handleRowCaptionClick(row: ExpandingRow): void; + + /** + * Handles expansion of a row. When a new row expands, we need to remove + * previous expansion and collapse. We also need to save the currently + * expanded row so that we can collapse this row once another row expands. + */ + handleRowExpand(row: ExpandingRow): void; + + /** + * Handles focus on a row. When a new row gets focus (note that this is + * different from expansion), we need to remove previous focus and expansion. + * We need to save the reference to this focused row so that we can unfocus + * this row when another row is focused. + */ + handleRowFocus(row: ExpandingRow): void; + + /** + * Function that is called by expanding row summary to focus on the last + * focusable element before the list of expanding rows. + */ + focusOnPreviousFocusableElement(): void; + + /** + * Function that is called by expanding row summary to focus on the next + * focusable element after the list of expanding rows. + */ + focusOnNextFocusableElement(): void; +} + +/** + * This component is used to render a single expanding row. It should contain + * cfc-expanding-row-summary, cfc-expanding-row-details-caption and + * cfc-expanding-row-details-content components. + */ +@Component({ + selector: 'cfc-expanding-row', + styles: [expanding_row_css], + template: ` +
+ +
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExpandingRow { + /** + * The identifier for this node provided by the user code. We need this + * while we are emitting onToggle event. + */ + @Input() rowId !: string; + + /** + * An ElementRef to the main element in this component. We need a reference + * to this element to compute the height. The height of cfc-expanding-row + * is used in [cfcExpandingRowHost] directive for scroll adjustments. + */ + @ViewChild('expandingRowMainElement', {static: true}) + expandingRowMainElement !: ElementRef; + + /** + * This @Output event emitter will be triggered when the user expands or + * collapses this node. + */ + @Output() onToggle = new EventEmitter(); + + /** + * A boolean indicating if this node is expanded. This value is used to + * hide/show summary, caption, and content of the expanding row. There should + * only be one expanded row within [cfcExpandingRowHost] directive. And if + * there is an expanded row, there shouldn't be any focused rows. + */ + set isExpanded(value: boolean) { + const changed: boolean = this.isExpandedInternal !== value; + this.isExpandedInternal = value; + + if (changed) { + this.isExpandedChange.emit(); + this.changeDetectorRef.markForCheck(); + } + } + + /** TS getter for isExpanded property. */ + get isExpanded(): boolean { return this.isExpandedInternal; } + + /** Triggered when isExpanded property changes. */ + isExpandedChange = new EventEmitter(); + + /** Triggered when index property changes. */ + indexChange = new EventEmitter(); + + /** + * A boolean indicating if this node is focused. This value is used to add + * a CSS class that should render a blue border on the right. There should + * only be one focused row in [cfcExpandingRowHost] directive. + */ + set isFocused(value: boolean) { + this.isFocusedInternal = value; + this.changeDetectorRef.markForCheck(); + } + + /** TS getter for isFocused property. */ + get isFocused(): boolean { return this.isFocusedInternal; } + + /** The index of the row in the context of the entire collection. */ + set index(value: number) { + const changed: boolean = this.indexInternal !== value; + this.indexInternal = value; + + if (changed) { + this.indexChange.emit(); + this.changeDetectorRef.markForCheck(); + } + } + + /** TS getter for index property. */ + get index(): number { return this.indexInternal; } + + /** + * We should probably rename this to summaryContentChild. Because technically + * this is not a @ViewChild that is in a template. This will be transcluded. + * Note that we are not using @ContentChild directive here. The @ContentChild + * will cause cyclic reference if the class definition for ExpandingRowSummary + * component is not in the same file as ExpandingRow. + */ + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + summaryViewChild !: ExpandingRowSummary; + + /** + * We compute the collapsed height (which is just height of + * cfc-expanding-row-summary component) in this component. This is used in + * [cfcExpandingRowHost] for scroll adjustment calculation. + */ + collapsedHeight = -1; + + /** Internal storage for isExpanded public property. */ + private isExpandedInternal = false; + + /** Internal storage for isFocused public property. */ + private isFocusedInternal = false; + + /** Internal storage for index public property. */ + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + private indexInternal !: number; + + /** + * This holds a reference to [cfcExpandingRowHost] directive. We need + * this reference to notify the host when this row expands/collapses or is + * focused. + */ + constructor( + public elementRef: ElementRef, + @Inject(EXPANDING_ROW_HOST_INJECTION_TOKEN) public expandingRowHost: ExpandingRowHostBase, + private readonly changeDetectorRef: ChangeDetectorRef) {} + + /** + * Handles click on cfc-expanding-row-summary component. This will expand + * this row and collapse the previously expanded row. The collapse & blur + * is handled in [cfcExpandingRowHost] directive. + */ + handleSummaryClick(): void { + this.collapsedHeight = + this.elementRef.nativeElement.querySelector('.cfc-expanding-row-summary').offsetHeight; + this.expandingRowHost.handleRowSummaryClick(this); + this.expand(); + } + + /** + * When user tabs into child cfc-expanding-row-summary component. This method + * will make sure we focuse on this row, and blur on previously focused row. + */ + handleSummaryFocus(): void { this.focus(); } + + /** + * cfc-expanding-row-details-caption component will call this function to + * notify click on its host element. Note that caption is only shown when + * the row is expanded. Hence this will collapse this row and put the focus + * on it. + * If a blacklisted element exists in the caption, clicking that element will + * not trigger the row collapse. + */ + handleCaptionClick(event: MouseEvent): void { + if (this.expandingRowHost.isBlacklisted(event.target as {} as HTMLElement)) { + return; + } + this.expandingRowHost.handleRowCaptionClick(this); + this.collapse(); + this.focus(); + } + + /** + * Gets the height of this component. This height is used in parent + * [cfcExpandingRowHost] directive to compute scroll adjustment. + */ + getHeight(): number { return this.expandingRowMainElement.nativeElement.offsetHeight; } + + /** + * Expands this row. This will notify the host so that it can collapse + * previously expanded row. This function also emits onToggle @Output event + * to the user code. + */ + expand(): void { + this.isExpanded = true; + this.expandingRowHost.handleRowExpand(this); + + // setTimeout here makes sure we scroll this row into view after animation. + setTimeout(() => { this.expandingRowMainElement.nativeElement.focus(); }); + + this.onToggle.emit({rowId: this.rowId, isExpand: true}); + } + + /** + * Collapses this row. Setting isExpanded to false will make sure we hide + * the caption and details, and show cfc-expanding-row-summary component. + * This also emits onToggle @Output event to the user code. + */ + collapse(): void { + this.isExpanded = false; + this.onToggle.emit({rowId: this.rowId, isExpand: false}); + } + + /** + * Blurs this row. This should remove the blue border on the left if there + * is any. This function will remove DOM focus on the + * cfc-expanding-row-summary + * component. + */ + blur(): void { + this.isFocused = false; + this.summaryViewChild.blur(); + } + + /** + * Focuses this row. This should put blue border on the left. If there is + * any previous focus/selection, those should be gone. Parent + * [cfcExpandingRowHost] component takes care of that. + */ + focus(): void { + this.isFocused = true; + this.expandingRowHost.handleRowFocus(this); + + // Summary child is not present currently. We need to NG2 to update the + // template. + setTimeout(() => { this.summaryViewChild.focus(); }); + } + + /** + * We listen for TAB press here to make sure we trap the focus on the + * expanded + * row. If the row is not expanded, we don't care about this event since focus + * trap should work for expanded rows only. + */ + @HostListener('keydown', ['$event']) + handleKeyDown(event: KeyboardEvent) { + const charCode = event.which || event.keyCode; + + switch (charCode) { + case 9: + if (!this.isExpanded) { + return; + } + + this.trapFocus(event); + break; + default: + break; + } + } + + /** + * When this row is expanded, this function traps the focus between focusable + * elements contained in this row. + */ + private trapFocus(event: KeyboardEvent): void { + const rowElement: HTMLElement = this.expandingRowMainElement.nativeElement; + const focusableEls: HTMLElement[] = []; + let lastFocusableEl: HTMLElement = rowElement; + + if (focusableEls.length) { + lastFocusableEl = focusableEls[focusableEls.length - 1]; + } + + if (event.target === lastFocusableEl && !event.shiftKey) { + rowElement.focus(); + event.preventDefault(); + } else if (event.target === rowElement && event.shiftKey) { + lastFocusableEl.focus(); + event.preventDefault(); + } + } +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts b/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts new file mode 100644 index 0000000000..6da0e5f31b --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_blacklist.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google Inc. 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 {Directive} from '@angular/core'; + + +/** + * This directive is used to flag an element to NOT trigger collapsing an + * expanded row + */ +@Directive({ + selector: '[cfcExpandingRowBlacklist]', +}) +export class ExpandingRowBlacklist { +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_css.ts b/modules/benchmarks/src/expanding_rows/expanding_row_css.ts new file mode 100644 index 0000000000..6eff25fba7 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_css.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +export const expanding_row_css = ` + ::ng-deep [cfcExpandingRowHost] { + display: block; + margin-bottom: 2; + } + + :host(cfc-expanding-row), + :host(cfc-expanding-row-summary), + :host(cfc-expanding-row-details-caption), + :host(cfc-expanding-row-details-content) { + display: block; + } + + .cfc-expanding-row { + background: white; + border-top: 1 solid black; + box-shadow: 0 1 1 gray; + transition: margin 1 1; + will-change: margin; + } + + .cfc-expanding-row.cfc-expanding-row-is-expanded { + margin: 1 (-1); + } + + .cfc-expanding-row:focus { + outline: none; + } + + .cfc-expanding-row-summary { + + display: flex; + border-left: 6 solid transparent; + cursor: pointer; + padding: 6 2; + + } + + .cfc-expanding-row-summary:focus { + outline: none; + border-left-color: $cfc-color-active; + } + + // Adjust icons to be positioned correctly in the row. + .cfc-expanding-row-summary::ng-deep cfc-icon { + margin-top: 3; + } + + .cfc-expanding-row-details-caption { + display: flex; + cursor: pointer; + padding: 4 2; + + } + + .cfc-expanding-row-details-caption::ng-deep a, + .cfc-expanding-row-details-caption::ng-deep a:visited, + .cfc-expanding-row-details-caption::ng-deep a .cfc-external-link-content { + border-color: $cfc-color-text-primary-inverse; + color: $cfc-color-text-primary-inverse; + } + + // Adjust icons to be positioned correctly in the row. + ::ng-deep cfc-icon { + margin-top: 3; + } + + .cfc-expanding-row-details-content { + padding: 2; + } + + .cfc-expanding-row-details-content::ng-deep .ace-kv-list.cfc-full-bleed { + width: 200px; + } + + + .cfc-expanding-row-accessibility-text { + display: none; + }`; diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts b/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts new file mode 100644 index 0000000000..15f77c95db --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_details_caption.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright Google Inc. 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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnDestroy} from '@angular/core'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; + +import {ExpandingRow} from './expanding_row'; +import {expanding_row_css} from './expanding_row_css'; + +/** + * This component should be within cfc-expanding-row component. The caption + * is only visible when the row is expanded. + */ +@Component({ + selector: 'cfc-expanding-row-details-caption', + styles: [expanding_row_css], + template: ` +
+ +
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExpandingRowDetailsCaption implements OnDestroy { + /** The background color of this component. */ + @Input() color: string = 'blue'; + + /** This is triggered when this component is destroyed. */ + private readonly onDestroy = new Subject(); + + /** + * We need a reference to parent cfc-expanding-row component here to hide + * this component when the row is collapsed. We also need to relay clicks + * to the parent component. + */ + constructor(@Host() public expandingRow: ExpandingRow, changeDetectorRef: ChangeDetectorRef) { + this.expandingRow.isExpandedChange.pipe(takeUntil(this.onDestroy)).subscribe(() => { + changeDetectorRef.markForCheck(); + }); + } + + /** When component is destroyed, unlisten to isExpanded. */ + ngOnDestroy(): void { this.onDestroy.next(); } +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts b/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts new file mode 100644 index 0000000000..2ab182a39c --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_details_content.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright Google Inc. 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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, OnDestroy} from '@angular/core'; +import {Subscription} from 'rxjs'; + +import {ExpandingRow} from './expanding_row'; +import {expanding_row_css} from './expanding_row_css'; + +/** + * This component should be within cfc-expanding-row component. Note that the + * content is visible only when the row is expanded. + */ +@Component({ + styles: [expanding_row_css], + selector: 'cfc-expanding-row-details-content', + template: ` +
+ +
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExpandingRowDetailsContent implements OnDestroy { + /** Used for unsubscribing to changes in isExpanded parent property. */ + private isExpandedChangeSubscription: Subscription; + + /** + * We need a reference to parent cfc-expanding-row component to make sure we + * hide this component if the row is collapsed. + */ + constructor(@Host() public expandingRow: ExpandingRow, changeDetectorRef: ChangeDetectorRef) { + this.isExpandedChangeSubscription = + this.expandingRow.isExpandedChange.subscribe(() => { changeDetectorRef.markForCheck(); }); + } + + /** Unsubscribe from changes in parent isExpanded property. */ + ngOnDestroy(): void { this.isExpandedChangeSubscription.unsubscribe(); } +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_host.ts b/modules/benchmarks/src/expanding_rows/expanding_row_host.ts new file mode 100644 index 0000000000..28a6ed15f9 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_host.ts @@ -0,0 +1,497 @@ +/** + * @license + * Copyright Google Inc. 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 {AfterContentInit, AfterViewInit, ChangeDetectionStrategy, Component, ContentChildren, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Output, QueryList, ViewChild, forwardRef} from '@angular/core'; +import {Subscription} from 'rxjs'; + +import {EXPANDING_ROW_HOST_INJECTION_TOKEN, ExpandingRow, ExpandingRowHostBase} from './expanding_row'; + + +/** + * We use this class in template to identify the row. + * The [cfcExpandingRowHost] directive also uses this class to check if a given + * HTMLElement is within an . + */ +const EXPANDING_ROW_CLASS_NAME = 'cfc-expanding-row'; + +/** Throttle duration in milliseconds for repeated key presses. */ +export const EXPANDING_ROW_KEYPRESS_THORTTLE_MS = 50; + +/** + * This type union is created to make arguments of handleUpOrDownPress* + * methods in ExpandingRowHost class more readable. + */ +type UpOrDown = 'up' | 'down'; + +/** + * This is the wrapper directive for the cfc-expanding-row components. Note that + * we wanted to make this a directive instead of component because child + * cfc-expanding-row components does not have to be a direct child. + */ +@Component({ + selector: 'cfc-expanding-row-host', + template: ` +
+
+ +
+
`, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{provide: EXPANDING_ROW_HOST_INJECTION_TOKEN, useExisting: ExpandingRowHost}], +}) +export class ExpandingRowHost implements AfterViewInit, + OnDestroy, ExpandingRowHostBase { + /** + * An HTML selector (e.g. "body") for the scroll element. We need this to + * make some scroll adjustments. + */ + @Input() scrollElementSelector = '.cfc-panel-body-scrollable'; + + /** + * An HTML selector (e.g. "body") for the click root. While the row is + * expanded, and user clicks outside of the expanded row, we collapse this row + * But to do this, we need to know the clickable area. + */ + @Input() clickRootElementSelector = 'cfc-panel-body'; + + /** + * The @Output will be triggered when the user wants to focus on the + * previously expanded row, and we are already at the first row. The logs team + * will use this to prepend data on demand. + */ + @Output() onPrepend = new EventEmitter(); + + /** A reference to the last focusable element in list of expanding rows. */ + @ViewChild('lastFocusable', {static: true}) lastFocusableElement !: ElementRef; + + /** A reference to the first focusable element in list of expanding rows. */ + @ViewChild('firstFocusable', {static: true}) + firstFocusableElement !: ElementRef; + + /** + * A reference to all child cfc-expanding-row elements. We will need for + * keyboard accessibility and scroll adjustments. For example, we need to know + * which row is previous row when user presses "left arrow" on a focused row. + */ + @ContentChildren(forwardRef(() => ExpandingRow), {descendants: true}) + contentRows !: QueryList; + + /** + * Keeps track of the last row that had focus before focus left the list + * of expanding rows. + */ + lastFocusedRow?: ExpandingRow = undefined; + + /** + * Focused rows just show a blue left border. This node is not expanded. We + * need to keep a reference to the focused row to unfocus when another row + * is focused. + */ + private focusedRow?: ExpandingRow = undefined; + + /** + * This is the expanded row. If there is an expanded row there shouldn't be + * any focused rows. We need a reference to this. For example we need to + * collapse the currently expanded row, if another row is expanded. + */ + private expandedRow?: ExpandingRow = undefined; + + /** + * This is just handleRootMouseUp.bind(this). handleRootMouseUp handles + * click events on root element (defined by clickRootElementSelector @Input) + * Since we attach the click listener dynamically, we need to keep this + * function around. This enables us to detach the click listener when + * component is destroyed. + */ + private handleRootMouseUpBound: EventListenerObject = this.handleRootMouseUp.bind(this); + + /** + * 16px is the margin animation we have on cfc-expanding-row component. + * We need this value to compute scroll adjustments. + */ + private static rowMargin = 16; + + /** Subscription to changes in the expanding rows. */ + // TODO(b/109816955): remove '!', see go/strict-prop-init-fix. + private rowChangeSubscription !: Subscription; + + /** + * When component initializes we need to attach click listener to the root + * element. This click listener will allows us to collapse the + * currently expanded row when user clicks outside of it. + */ + ngAfterViewInit(): void { + const clickRootElement: HTMLElement = this.getClickRootElement(); + + if (!clickRootElement) { + return; + } + + clickRootElement.addEventListener('mouseup', this.handleRootMouseUpBound); + + this.rowChangeSubscription = + this.contentRows.changes.subscribe(() => { this.recalcRowIndexes(); }); + this.recalcRowIndexes(); + } + + /** + * Detaches the click listener on the root element. Note that we are attaching + * this listener on ngAfterViewInit function. + */ + ngOnDestroy(): void { + const clickRootElement: HTMLElement = this.getClickRootElement(); + + if (!clickRootElement) { + return; + } + + clickRootElement.removeEventListener('mouseup', this.handleRootMouseUpBound); + + if (this.rowChangeSubscription) { + this.rowChangeSubscription.unsubscribe(); + } + } + + /** + * Handles caption element click on a cfc-expanding-row component. Note + * that caption element is visible only when the row is expanded. So this + * means we will collapse the expanded row. The scroll adjustment below + * makes sure that the mouse stays under the summary of the expanded row + * when the row collapses. + */ + handleRowCaptionClick(row: ExpandingRow): void { + const scrollAdjustment: number = -ExpandingRowHost.rowMargin; + const scrollElement: HTMLElement = this.getScrollElement() as HTMLElement; + if (!scrollElement) { + return; + } + + scrollElement.scrollTop += scrollAdjustment; + } + + /** + * Handles summary element click on a cfc-expanding-row component. Note + * that summary element is visible only when the row is collapsed. So this + * event will fired prior to expansion of a collapsed row. Scroll adjustment + * below makes sure mouse stays on the caption element when the collapsed + * row expands. + */ + handleRowSummaryClick(row: ExpandingRow): void { + const hadPreviousSelection: boolean = !!this.expandedRow; + const previousSelectedRowIndex: number = this.getRowIndex(this.expandedRow as ExpandingRow); + const newSelectedRowIndex: number = this.getRowIndex(row); + const previousCollapsedHeight: number = this.getSelectedRowCollapsedHeight(); + const previousExpansionHeight = this.getSelectedRowExpandedHeight(); + + if (this.expandedRow) { + return; + } + + let scrollAdjustment = 0; + const scrollElement: HTMLElement = this.getScrollElement() as HTMLElement; + if (!scrollElement) { + return; + } + + if (previousExpansionHeight > 0 && previousCollapsedHeight >= 0) { + scrollAdjustment = previousExpansionHeight - previousCollapsedHeight; + } + + const newSelectionIsInfrontOfPrevious: boolean = newSelectedRowIndex > previousSelectedRowIndex; + const multiplier = newSelectionIsInfrontOfPrevious ? -1 : 0; + scrollAdjustment = scrollAdjustment * multiplier + ExpandingRowHost.rowMargin; + + scrollElement.scrollTop += scrollAdjustment; + } + + /** + * Handles expansion of a row. When a new row expands, we need to remove + * previous expansion and collapse. We also need to save the currently + * expanded row so that we can collapse this row once another row expands. + */ + handleRowExpand(row: ExpandingRow): void { + this.removePreviousFocus(); + this.removePreviousExpansion(); + this.expandedRow = row; + } + + /** + * Handles focus on a row. When a new row gets focus (note that this is + * different from expansion), we need to remove previous focus and expansion. + * We need to save the reference to this focused row so that we can unfocus + * this row when another row is focused. + */ + handleRowFocus(row: ExpandingRow): void { + // Do not blur then refocus the row if it's already selected. + if (row === this.focusedRow) { + return; + } + + this.removePreviousFocus(); + this.removePreviousExpansion(); + this.focusedRow = row; + } + + /** + * Called when shift+tabbing from the first focusable element after the list + * of expanding rows or tabbing from the last focusable element before. + */ + focusOnLastFocusedRow(): void { + if (!this.lastFocusedRow) { + this.lastFocusedRow = this.contentRows.toArray()[0]; + } + this.lastFocusedRow.focus(); + } + + /** + * Function that is called by expanding row summary to focus on the last + * focusable element before the list of expanding rows. + */ + focusOnPreviousFocusableElement(): void { this.lastFocusedRow = this.focusedRow; } + + /** + * Function that is called by expanding row summary to focus on the next + * focusable element after the list of expanding rows. + */ + focusOnNextFocusableElement(): void { this.lastFocusedRow = this.focusedRow; } + + /** + * Handles keydown event on the host. We are just concerned with up, + * down arrow, ESC, and ENTER presses here. Note that Up/Down presses + * can be repeated. + * + * - Up: Focuses on the row above. + * - Down: Focuses on the row below. + * - Escape: Collapses the expanded row. + * - Enter: Expands the focused row. + */ + @HostListener('keydown', ['$event']) + handleKeyDown(event: KeyboardEvent) {} + + /** + * Recursively returns true if target HTMLElement is within a + * cfc-expanding-row component. It will return false otherwise. + * We need this function in handleRootMouseUp to collapse the expanded row + * when user clicks outside of all expanded rows. + */ + private isTargetInRow(target: HTMLElement): boolean { + return target.classList.contains(EXPANDING_ROW_CLASS_NAME); + } + + /** + * Gets the click root element that is described by clickRootElementSelector + * @Input value. + */ + private getClickRootElement(): HTMLElement { + return document.querySelector(this.clickRootElementSelector) as HTMLElement; + } + + /** + * Handles all of the mouseup events on the click root. When user clicks + * outside of an expanded row, we need to collapse that row. + * We trigger collapse by calling handleCaptionClick() on the expanded row. + */ + private handleRootMouseUp(event: MouseEvent): void { + if (!this.expandedRow) { + return; + } + + if (!this.isTargetInRow(event.target as {} as HTMLElement)) { + this.expandedRow.handleCaptionClick(event); + } + } + + /** + * Check if element is blacklisted. Blacklisted elements will not collapse an + * open row when clicked. + */ + isBlacklisted(element: HTMLElement|null): boolean { + const clickRoot = this.getClickRootElement(); + while (element && element !== clickRoot) { + if (element.hasAttribute('cfcexpandingrowblacklist')) { + return true; + } + element = element.parentElement; + } + return false; + } + + /** + * Removes focus state from a previously focused row. We blur this row and + * set the focusedRow to undefined in this method. This usually happens when + * another row is focused. + */ + private removePreviousFocus(): void { + if (this.focusedRow) { + this.focusedRow.blur(); + this.focusedRow = undefined; + } + } + + /** + * Removes the expanded state from a previously expanded row. We collapse this + * row and set the expandedRow to undefined in this method. This usually + * happens when another row is expanded. + */ + private removePreviousExpansion(): void { + if (this.expandedRow) { + this.expandedRow.collapse(); + this.expandedRow = undefined; + } + } + + /** + * Gets the collapsed height of the currently expanded row. We need this for + * scroll adjustments. Note that collapsed height of a cfc-expanding-row + * component is equal to height of cfc-expanding-row-summary component within + * the row. + */ + private getSelectedRowCollapsedHeight(): number { + if (this.expandedRow) { + return this.expandedRow.collapsedHeight; + } else { + return -1; + } + } + + /** + * Gets the current height of the expanded row. We need this value for the + * scroll adjustment computation. + */ + private getSelectedRowExpandedHeight(): number { + if (this.expandedRow) { + return this.expandedRow.getHeight(); + } else { + return -1; + } + } + + /** + * Gets the HTML element described by scrollElementSelector @Input value. + * We need this value for scroll adjustments. + */ + private getScrollElement(): HTMLElement|undefined { + if (!this.scrollElementSelector) { + return undefined; + } + + return document.querySelector(this.scrollElementSelector) as HTMLElement; + } + + /** + * Handles escape presses on the host element. Escape removes previous focus + * if there is one. If there is an expanded row, escape row collapses this + * row and focuses on it. A subsequent escape press will blur this row. + */ + private handleEscapePress(): void { + this.removePreviousFocus(); + + if (this.expandedRow) { + this.expandedRow.collapse(); + this.expandedRow.focus(); + this.expandedRow = undefined; + } + } + + /** + * Handles enter keypress. If there is a focused row, an enter key press on + * host element will expand this row. + */ + private handleEnterPress(): void { + if (document.activeElement !== this.focusedRowSummary()) { + return; + } + + if (this.focusedRow) { + this.focusedRow.expand(); + } + } + + /** Returns the HTMLElement that is the currently focused row summary. */ + private focusedRowSummary(): HTMLElement|undefined { + return this.focusedRow ? this.focusedRow.summaryViewChild.mainElementRef.nativeElement : + undefined; + } + + /** + * Returns the index of a given row. This enables us to figure out the row + * above/below the focused row. + */ + private getRowIndex(rowToLookFor: ExpandingRow): number { + return rowToLookFor ? rowToLookFor.index : -1; + } + + /** + * Handles up/down arrow presses on the host element. Up arrow press will + * focus/expand on the row above. Down arrow press will focus/expand the row + * below. If we have a focus on the current row, this function will focus on + * the computed (the one above or below) row. If host has an expanded row, + * this function will expand the computed row. + */ + private handleUpOrDownPressOnce(upOrDown: UpOrDown, event: KeyboardEvent): void { + event.preventDefault(); + + // If row is expanded but focus is inside the expanded element, arrow + // key presses should not do anything. + if (this.expandedRow && + document.activeElement !== this.expandedRow.expandingRowMainElement.nativeElement) { + return; + } + + // If focus is inside a collapsed row header, arrow key presses should not + // do anything. + if (this.focusedRow && document.activeElement !== this.focusedRowSummary()) { + return; + } + // We only want screen reader to read the message the first time we enter + // the list of expanding rows, so we must reset the variable here + this.lastFocusedRow = undefined; + + const rowToLookFor: ExpandingRow|undefined = this.expandedRow || this.focusedRow; + if (!rowToLookFor) { + return; + } + + const isFocus: boolean = (rowToLookFor === this.focusedRow); + + const rowIndex: number = this.getRowIndex(rowToLookFor); + const contentRowsArray: ExpandingRow[] = this.contentRows.toArray(); + + if (rowIndex < 0) { + return; + } + + const potentialIndex: number = (upOrDown === 'up' ? -1 : +1) + rowIndex; + if (potentialIndex < 0) { + this.onPrepend.emit(); + return; + } + + if (potentialIndex >= contentRowsArray.length) { + return; + } + + const potentialRow: ExpandingRow = contentRowsArray[potentialIndex]; + if (isFocus) { + potentialRow.focus(); + } else { + potentialRow.expand(); + } + } + + // Updates all of the rows with their new index. + private recalcRowIndexes() { + let index = 0; + setTimeout( + () => { this.contentRows.forEach((row: ExpandingRow) => { row.index = index++; }); }); + } +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_module.ts b/modules/benchmarks/src/expanding_rows/expanding_row_module.ts new file mode 100644 index 0000000000..13ced09aa4 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_module.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. 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 {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; + +import {ExpandingRow} from './expanding_row'; +import {ExpandingRowBlacklist} from './expanding_row_blacklist'; +import {ExpandingRowDetailsCaption} from './expanding_row_details_caption'; +import {ExpandingRowDetailsContent} from './expanding_row_details_content'; +import {ExpandingRowHost} from './expanding_row_host'; +import {ExpandingRowSummary} from './expanding_row_summary'; + +/** The main module for the cfc-expanding-row component. */ +@NgModule({ + declarations: [ + ExpandingRow, + ExpandingRowDetailsCaption, + ExpandingRowDetailsContent, + ExpandingRowHost, + ExpandingRowSummary, + ExpandingRowBlacklist, + ], + exports: [ + ExpandingRow, + ExpandingRowDetailsCaption, + ExpandingRowDetailsContent, + ExpandingRowHost, + ExpandingRowSummary, + ExpandingRowBlacklist, + ], + imports: [ + CommonModule, + ], +}) +export class ExpandingRowModule { +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts b/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts new file mode 100644 index 0000000000..172b97ee49 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_summary.ts @@ -0,0 +1,207 @@ +/** + * @license + * Copyright Google Inc. 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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Host, HostListener, OnDestroy, ViewChild} from '@angular/core'; +import {Subscription} from 'rxjs'; + +import {ExpandingRow} from './expanding_row'; +import {expanding_row_css} from './expanding_row_css'; + +const KEY_CODE_TAB = 9; + +/** + * This component should be used within cfc-expanding-row component. Note that + * summary is visible only when the row is collapsed. + */ +@Component({ + selector: 'cfc-expanding-row-summary', + styles: [expanding_row_css], + template: ` +
+ +
.
+
+ Row {{expandingRow.index + 1}} in list of expanding rows. +
+
+ Use arrow keys to navigate. +
+
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExpandingRowSummary implements OnDestroy { + /** + * A reference to the main element. This element should be focusable. We need + * reference to compute collapsed height of the row. We also use this + * reference for focus and blur methods below. + */ + @ViewChild('expandingRowSummaryMainElement', {static: false}) + mainElementRef !: ElementRef; + + /** Subscription for changes in parent isExpanded property. */ + private isExpandedSubscription: Subscription; + + /** Subscription for changes in parent index property. */ + private indexSubscription: Subscription; + + /** + * We need the parent cfc-expanding-row component here to hide this element + * when the row is expanded. cfc-expanding-row-details-caption element + * will act as a header for expanded rows. We also need to relay tab-in and + * click events to the parent. + */ + constructor(@Host() public expandingRow: ExpandingRow, changeDetectorRef: ChangeDetectorRef) { + this.expandingRow.summaryViewChild = this; + this.isExpandedSubscription = + this.expandingRow.isExpandedChange.subscribe(() => { changeDetectorRef.markForCheck(); }); + + this.indexSubscription = + this.expandingRow.indexChange.subscribe(() => { changeDetectorRef.markForCheck(); }); + } + + + /** When component is destroyed, unlisten to isExpanded. */ + ngOnDestroy(): void { + if (this.isExpandedSubscription) { + this.isExpandedSubscription.unsubscribe(); + } + if (this.indexSubscription) { + this.indexSubscription.unsubscribe(); + } + } + + /** + * Handles focus event on the element. We basically want to detect any focus + * in this component and relay this information to parent cfc-expanding-row + * component. + */ + handleFocus(): void { + // Clicking causes a focus event to occur before the click event. Filter + // out click events using the cdkFocusMonitor. + // + // TODO(b/62385992) Use the KeyboardFocusService to detect focus cause + // instead of creating multiple monitors on a page. + if (this.expandingRow.expandingRowMainElement.nativeElement.classList.contains( + 'cdk-mouse-focused')) { + return; + } + + if (!this.expandingRow.isFocused && !this.expandingRow.isExpanded) { + this.expandingRow.handleSummaryFocus(); + } + } + + /** + * Handles tab & shift+tab presses on expanding row summaries in case there + * are tabbable elements inside the summaries. + */ + @HostListener('keydown', ['$event']) + handleKeyDown(event: KeyboardEvent) { + const charCode = event.which || event.keyCode; + if (charCode === KEY_CODE_TAB) { + this.handleTabKeypress(event); + } + } + + /** + * Handles tab and shift+tab presses inside expanding row summaries; + * + * From inside collapsed row summary: + * - Tab: If focus was on the last focusable child, should shift focus to + * the next focusable element outside the list of expanding rows. + * - Shift+tab: If focus was on first focusable child, should shift focus to + * the main collapsed row summary element + * If focus was on main collapsed row summary element, should + * shift focus to the last focusable element before the list of + * expanding rows. + */ + handleTabKeypress(event: KeyboardEvent): void { + const focusableChildren = this.getFocusableChildren(); + + if (focusableChildren.length === 0) { + return; + } + + // Shift+tab on expanding row summary should focus on last focusable element + // before expanding row list. Otherwise, if shift+tab is pressed on first + // focusable child inside expanding row summary, it should focus on main + // expanding row summary element. + if (event.shiftKey && document.activeElement === this.mainElementRef.nativeElement) { + event.preventDefault(); + this.expandingRow.expandingRowHost.focusOnPreviousFocusableElement(); + return; + } else if (event.shiftKey && document.activeElement === focusableChildren[0]) { + event.preventDefault(); + this.expandingRow.focus(); + } + + // If tab is pressed on the last focusable element inside an expanding row + // summary, focus should be set to the next focusable element after the list + // of expanding rows. + if (!event.shiftKey && + document.activeElement === focusableChildren[focusableChildren.length - 1]) { + event.preventDefault(); + this.expandingRow.expandingRowHost.focusOnNextFocusableElement(); + } + } + + /** + * Finds the row that had focus before focus left the list of expanding rows + * and checks if the current row summary is that row. + */ + isPreviouslyFocusedRow(): boolean { + if (!this.expandingRow.expandingRowHost.contentRows) { + return false; + } + + const expandingRowHost = this.expandingRow.expandingRowHost; + + if (!this.mainElementRef || !expandingRowHost.lastFocusedRow) { + return false; + } + + if (!expandingRowHost.lastFocusedRow.summaryViewChild.mainElementRef) { + return false; + } + + // If the current expanding row summary was the last focused one before + // focus exited the list, then return true to trigger the screen reader + if (this.mainElementRef.nativeElement === + expandingRowHost.lastFocusedRow.summaryViewChild.mainElementRef.nativeElement) { + return true; + } + return false; + } + + /** Puts the DOM focus on the main element. */ + focus(): void { + if (this.mainElementRef && document.activeElement !== this.mainElementRef.nativeElement) { + this.mainElementRef.nativeElement.focus(); + } + } + + /** Removes the DOM focus on the main element. */ + blur(): void { + if (!this.mainElementRef) { + return; + } + + this.mainElementRef.nativeElement.blur(); + } + + /** Returns array of focusable elements within this component. */ + private getFocusableChildren(): HTMLElement[] { return []; } +} diff --git a/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts b/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts new file mode 100644 index 0000000000..e1b1e334fc --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/expanding_row_toggle_event.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +/** + * This interface is used to send toggle (expand/collapse) events to the user + * code. + */ +export interface ExpandingRowToggleEvent { + /** The identifier of the row that was toggled. */ + rowId: string; + + /** + * A boolean indicating whether or not this row was expanded. This is set to + * false if the row was collapsed. + */ + isExpand: boolean; +} diff --git a/modules/benchmarks/src/expanding_rows/index.html b/modules/benchmarks/src/expanding_rows/index.html new file mode 100644 index 0000000000..402a1fc8d8 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + +

Change Detection Benchmark

+
...
+ + loading... + + + + + \ No newline at end of file diff --git a/modules/benchmarks/src/expanding_rows/index.ts b/modules/benchmarks/src/expanding_rows/index.ts new file mode 100644 index 0000000000..743f0ee323 --- /dev/null +++ b/modules/benchmarks/src/expanding_rows/index.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google Inc. 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 {enableProdMode} from '@angular/core'; +import {platformBrowser} from '@angular/platform-browser'; + +import {ExpandingRowBenchmarkModule} from './benchmark'; +import {ExpandingRowBenchmarkModuleNgFactory} from './benchmark.ngfactory'; + +setMode(ExpandingRowBenchmarkModule.hasOwnProperty('ngModuleDef') ? 'Ivy' : 'ViewEngine'); +enableProdMode(); +platformBrowser().bootstrapModuleFactory(ExpandingRowBenchmarkModuleNgFactory); + +function setMode(name: string): void { + document.querySelector('#rendererMode') !.textContent = `Render Mode: ${name}`; +} \ No newline at end of file diff --git a/package.json b/package.json index 5bee6f8745..3b9e8567af 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "angular-srcs", - "version": "8.0.0-rc.0", + "version": "8.1.0-beta.0", "private": true, - "branchPattern": "2.0.*", "description": "Angular - a web framework for modern web apps", "homepage": "https://github.com/angular/angular", "bugs": "https://github.com/angular/angular/issues", @@ -36,9 +35,9 @@ "@angular-devkit/core": "^8.0.0-beta.15", "@angular-devkit/schematics": "^8.0.0-beta.15", "@angular/bazel": "file:./tools/npm/@angular_bazel", - "@bazel/jasmine": "0.27.12", - "@bazel/karma": "0.27.12", - "@bazel/typescript": "0.27.12", + "@bazel/jasmine": "0.29.0", + "@bazel/karma": "0.29.0", + "@bazel/typescript": "0.29.0", "@microsoft/api-extractor": "^7.0.21", "@schematics/angular": "^8.0.0-beta.15", "@types/angular": "^1.6.47", @@ -94,6 +93,7 @@ "rollup": "^1.1.0", "rollup-plugin-amd": "^3.0.0", "rollup-plugin-commonjs": "^9.2.1", + "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^4.0.0", "rollup-plugin-sourcemaps": "^0.4.2", "rxjs": "^6.4.0", @@ -117,12 +117,13 @@ "// 3": "when updating @bazel/bazel version you also need to update the RBE settings in .bazelrc (see https://github.com/angular/angular/pull/27935)", "devDependencies": { "@angular/cli": "^8.0.0-beta.15", - "@bazel/bazel": "0.24.0", + "@bazel/bazel": "0.26.0-rc5", "@bazel/buildifier": "^0.19.2", "@bazel/ibazel": "~0.9.0", "@types/minimist": "^1.2.0", "@types/systemjs": "0.19.32", "browserstacktunnel-wrapper": "2.0.1", + "check-side-effects": "0.0.20", "clang-format": "1.0.41", "cldr": "4.10.0", "cldr-data-downloader": "0.3.2", @@ -153,6 +154,7 @@ "sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz", "semver": "5.4.1", "tslint-eslint-rules": "4.1.1", + "tslint-no-toplevel-property-access": "0.0.2", "tsutils": "2.27.2", "universal-analytics": "0.4.15", "vlq": "0.2.2", diff --git a/packages/animations/browser/src/dsl/style_normalization/web_animations_style_normalizer.ts b/packages/animations/browser/src/dsl/style_normalization/web_animations_style_normalizer.ts index 2a79684e90..9cefed0d08 100644 --- a/packages/animations/browser/src/dsl/style_normalization/web_animations_style_normalizer.ts +++ b/packages/animations/browser/src/dsl/style_normalization/web_animations_style_normalizer.ts @@ -34,9 +34,10 @@ export class WebAnimationsStyleNormalizer extends AnimationStyleNormalizer { } } -const DIMENSIONAL_PROP_MAP = makeBooleanMap( - 'width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent,perspective' - .split(',')); +const DIMENSIONAL_PROP_MAP = + (() => makeBooleanMap( + 'width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent,perspective' + .split(',')))(); function makeBooleanMap(keys: string[]): {[key: string]: boolean} { const map: {[key: string]: boolean} = {}; diff --git a/packages/animations/browser/src/render/shared.ts b/packages/animations/browser/src/render/shared.ts index 9d769e0b2f..81f769d713 100644 --- a/packages/animations/browser/src/render/shared.ts +++ b/packages/animations/browser/src/render/shared.ts @@ -158,16 +158,20 @@ if (_isNode || typeof Element !== 'undefined') { // this is well supported in all browsers _contains = (elm1: any, elm2: any) => { return elm1.contains(elm2) as boolean; }; - if (_isNode || Element.prototype.matches) { - _matches = (element: any, selector: string) => element.matches(selector); - } else { - const proto = Element.prototype as any; - const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || - proto.oMatchesSelector || proto.webkitMatchesSelector; - if (fn) { - _matches = (element: any, selector: string) => fn.apply(element, [selector]); + _matches = (() => { + if (_isNode || Element.prototype.matches) { + return (element: any, selector: string) => element.matches(selector); + } else { + const proto = Element.prototype as any; + const fn = proto.matchesSelector || proto.mozMatchesSelector || proto.msMatchesSelector || + proto.oMatchesSelector || proto.webkitMatchesSelector; + if (fn) { + return (element: any, selector: string) => fn.apply(element, [selector]); + } else { + return _matches; + } } - } + })(); _query = (element: any, selector: string, multi: boolean): any[] => { let results: any[] = []; diff --git a/packages/bazel/package.bzl b/packages/bazel/package.bzl index 8d9e75f841..d010eef32e 100644 --- a/packages/bazel/package.bzl +++ b/packages/bazel/package.bzl @@ -40,12 +40,14 @@ def rules_angular_dev_dependencies(): ############################################# http_archive( name = "io_bazel_rules_sass", + sha256 = "76ae498b9a96fa029f026f8358ed44b93c934dde4691a798cb3a4137c307b7dc", strip_prefix = "rules_sass-1.15.1", url = "https://github.com/bazelbuild/rules_sass/archive/1.15.1.zip", ) http_archive( name = "io_bazel_skydoc", + sha256 = "f88058b43112e9bdc7fdb0abbdc17c5653268708c01194a159641119195e45c6", strip_prefix = "skydoc-a9550cb3ca3939cbabe3b589c57b6f531937fa99", # TODO: switch to upstream when https://github.com/bazelbuild/skydoc/pull/103 is merged url = "https://github.com/alexeagle/skydoc/archive/a9550cb3ca3939cbabe3b589c57b6f531937fa99.zip", diff --git a/packages/bazel/src/BUILD.bazel b/packages/bazel/src/BUILD.bazel index 4dba862e0a..3e0adfee11 100644 --- a/packages/bazel/src/BUILD.bazel +++ b/packages/bazel/src/BUILD.bazel @@ -18,6 +18,7 @@ nodejs_binary( "@npm//rollup", "@npm//rollup-plugin-amd", "@npm//rollup-plugin-commonjs", + "@npm//rollup-plugin-json", "@npm//rollup-plugin-node-resolve", "@npm//rollup-plugin-sourcemaps", ], diff --git a/packages/bazel/src/builders/bazel.ts b/packages/bazel/src/builders/bazel.ts index 5a15c6503f..e2dd5cb82f 100644 --- a/packages/bazel/src/builders/bazel.ts +++ b/packages/bazel/src/builders/bazel.ts @@ -9,7 +9,8 @@ /// import {spawn} from 'child_process'; -import {copyFileSync, existsSync, readdirSync, statSync, unlinkSync} from 'fs'; +import {copyFileSync, existsSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync} from 'fs'; +import {platform} from 'os'; import {dirname, join, normalize} from 'path'; export type Executable = 'bazel' | 'ibazel'; @@ -106,6 +107,51 @@ function listR(dir: string): string[] { return list(dir, '', []); } +/** + * Return the name of the lock file that is present in the specified 'root' + * directory. If none exists, default to creating an empty yarn.lock file. + */ +function getOrCreateLockFile(root: string): 'yarn.lock'|'package-lock.json' { + const yarnLock = join(root, 'yarn.lock'); + if (existsSync(yarnLock)) { + return 'yarn.lock'; + } + const npmLock = join(root, 'package-lock.json'); + if (existsSync(npmLock)) { + return 'package-lock.json'; + } + // Prefer yarn if no lock file exists + writeFileSync(yarnLock, ''); + return 'yarn.lock'; +} + +// Replace yarn_install rule with npm_install and copy from 'source' to 'dest'. +function replaceYarnWithNpm(source: string, dest: string) { + const srcContent = readFileSync(source, 'utf-8'); + const destContent = srcContent.replace(/yarn_install/g, 'npm_install') + .replace('yarn_lock', 'package_lock_json') + .replace('yarn.lock', 'package-lock.json'); + writeFileSync(dest, destContent); +} + +/** + * Disable sandbox on Mac OS by setting spawn_strategy in .bazelrc. + * For a hello world (ng new) application, removing the sandbox improves build + * time by almost 40%. + * ng build with sandbox: 22.0 seconds + * ng build without sandbox: 13.3 seconds + */ +function disableSandbox(source: string, dest: string) { + const srcContent = readFileSync(source, 'utf-8'); + const destContent = `${srcContent} +# Disable sandbox on Mac OS for performance reason. +build --spawn_strategy=local +run --spawn_strategy=local +test --spawn_strategy=local +`; + writeFileSync(dest, destContent); +} + /** * Copy Bazel files (WORKSPACE, BUILD.bazel, etc) from the template directory to * the project `root` directory, and return the absolute paths of the files @@ -117,6 +163,7 @@ export function copyBazelFiles(root: string, templateDir: string) { templateDir = normalize(templateDir); const bazelFiles: string[] = []; const templates = listR(templateDir); + const useYarn = getOrCreateLockFile(root) === 'yarn.lock'; for (const template of templates) { const name = template.replace('__dot__', '.').replace('.template', ''); @@ -124,7 +171,13 @@ export function copyBazelFiles(root: string, templateDir: string) { const dest = join(root, name); try { if (!existsSync(dest)) { - copyFileSync(source, dest); + if (!useYarn && name === 'WORKSPACE') { + replaceYarnWithNpm(source, dest); + } else if (platform() === 'darwin' && name === '.bazelrc') { + disableSandbox(source, dest); + } else { + copyFileSync(source, dest); + } bazelFiles.push(dest); } } catch { diff --git a/packages/bazel/src/builders/files/WORKSPACE.template b/packages/bazel/src/builders/files/WORKSPACE.template index d87aa93005..cb7bf141f2 100644 --- a/packages/bazel/src/builders/files/WORKSPACE.template +++ b/packages/bazel/src/builders/files/WORKSPACE.template @@ -12,8 +12,8 @@ workspace(name = "project") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") -RULES_NODEJS_VERSION = "0.27.12" -RULES_NODEJS_SHA256 = "3a3efbf223f6de733475602844ad3a8faa02abda25ab8cfe1d1ed0db134887cf" +RULES_NODEJS_VERSION = "0.29.0" +RULES_NODEJS_SHA256 = "1db950bbd27fb2581866e307c0130983471d4c3cd49c46063a2503ca7b6770a4" http_archive( name = "build_bazel_rules_nodejs", sha256 = RULES_NODEJS_SHA256, diff --git a/packages/bazel/src/esm5.bzl b/packages/bazel/src/esm5.bzl index 57d2157ca3..98463cb168 100644 --- a/packages/bazel/src/esm5.bzl +++ b/packages/bazel/src/esm5.bzl @@ -108,7 +108,7 @@ def _esm5_outputs_aspect(target, ctx): ctx.actions.run( progress_message = "Compiling TypeScript (ES5 with ES Modules) %s" % target.label, - inputs = target.typescript.replay_params.inputs + [tsconfig], + inputs = target.typescript.replay_params.inputs.to_list() + [tsconfig], outputs = outputs, arguments = [tsconfig.path], executable = compiler, diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 144fdf08d9..d0183c9b47 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -427,7 +427,7 @@ def ngc_compile_action( if is_legacy_ngc and messages_out != None: ctx.actions.run( - inputs = list(inputs), + inputs = inputs, outputs = messages_out, executable = ctx.executable.ng_xi18n, arguments = (_EXTRA_NODE_OPTIONS_FLAGS + @@ -442,7 +442,7 @@ def ngc_compile_action( if dts_bundles_out != None: # combine the inputs and outputs and filter .d.ts and json files - filter_inputs = [f for f in inputs + outputs if f.path.endswith(".d.ts") or f.path.endswith(".json")] + filter_inputs = [f for f in list(inputs) + outputs if f.path.endswith(".d.ts") or f.path.endswith(".json")] if _should_produce_flat_module_outs(ctx): dts_entry_points = ["%s.d.ts" % _flat_module_out_file(ctx)] @@ -489,6 +489,14 @@ def _compile_action(ctx, inputs, outputs, dts_bundles_out, messages_out, tsconfi # Give the Angular compiler all the user-listed assets file_inputs = list(ctx.files.assets) + if (type(inputs) == type([])): + file_inputs.extend(inputs) + else: + # inputs ought to be a list, but allow depset as well + # so that this can change independently of rules_typescript + # TODO(alexeagle): remove this case after update (July 2019) + file_inputs.extend(inputs.to_list()) + if hasattr(ctx.attr, "node_modules"): file_inputs.extend(_filter_ts_inputs(ctx.files.node_modules)) @@ -508,7 +516,7 @@ def _compile_action(ctx, inputs, outputs, dts_bundles_out, messages_out, tsconfi # Collect the inputs and summary files from our deps action_inputs = depset( file_inputs, - transitive = [inputs] + [ + transitive = [ dep.collect_summaries_aspect_result for dep in ctx.attr.deps if hasattr(dep, "collect_summaries_aspect_result") diff --git a/packages/bazel/src/ng_package/ng_package.bzl b/packages/bazel/src/ng_package/ng_package.bzl index f1a3c7a788..c00b44bec0 100644 --- a/packages/bazel/src/ng_package/ng_package.bzl +++ b/packages/bazel/src/ng_package/ng_package.bzl @@ -289,12 +289,13 @@ def _ng_package_impl(ctx): for d in ctx.attr.deps: if NodeModuleInfo in d: node_modules_files += _filter_js_inputs(d.files) + esm5_rollup_inputs = depset(node_modules_files, transitive = [esm5_sources]) esm2015_config = write_rollup_config(ctx, [], "/".join([ctx.bin_dir.path, ctx.label.package, _esm2015_root_dir(ctx)]), filename = "_%s.rollup_esm2015.conf.js") esm5_config = write_rollup_config(ctx, [], "/".join([ctx.bin_dir.path, ctx.label.package, esm5_root_dir(ctx)]), filename = "_%s.rollup_esm5.conf.js") - fesm2015.append(_rollup(ctx, "fesm2015", esm2015_config, es2015_entry_point, esm_2015_files + node_modules_files, fesm2015_output)) - fesm5.append(_rollup(ctx, "fesm5", esm5_config, es5_entry_point, esm5_sources + node_modules_files, fesm5_output)) + fesm2015.append(_rollup(ctx, "fesm2015", esm2015_config, es2015_entry_point, depset(node_modules_files, transitive = [esm_2015_files]), fesm2015_output)) + fesm5.append(_rollup(ctx, "fesm5", esm5_config, es5_entry_point, esm5_rollup_inputs, fesm5_output)) bundles.append( _rollup( @@ -302,7 +303,7 @@ def _ng_package_impl(ctx): "umd", esm5_config, es5_entry_point, - esm5_sources + node_modules_files, + esm5_rollup_inputs, umd_output, format = "umd", package_name = package_name, diff --git a/packages/bazel/src/ng_rollup_bundle.bzl b/packages/bazel/src/ng_rollup_bundle.bzl index d1100520e6..46f9641ead 100644 --- a/packages/bazel/src/ng_rollup_bundle.bzl +++ b/packages/bazel/src/ng_rollup_bundle.bzl @@ -18,7 +18,6 @@ load( "@build_bazel_rules_nodejs//internal/rollup:rollup_bundle.bzl", "ROLLUP_ATTRS", "ROLLUP_DEPS_ASPECTS", - "ROLLUP_OUTPUTS", "run_rollup", "run_sourcemapexplorer", "run_terser", @@ -27,6 +26,18 @@ load( load("@build_bazel_rules_nodejs//internal/common:collect_es6_sources.bzl", collect_es2015_sources = "collect_es6_sources") load(":esm5.bzl", "esm5_outputs_aspect", "esm5_root_dir", "flatten_esm5") +ROLLUP_OUTPUTS = { + "build_cjs": "%{name}.cjs.js", + "build_es2015": "%{name}.es2015.js", + "build_es2015_min": "%{name}.min.es2015.js", + "build_es2015_min_debug": "%{name}.min_debug.es2015.js", + "build_es5": "%{name}.js", + "build_es5_min": "%{name}.min.js", + "build_es5_min_debug": "%{name}.min_debug.js", + "build_umd": "%{name}.umd.js", + "explore_html": "%{name}.explore.html", +} + PACKAGES = [ # Generated paths when using ng_rollup_bundle outside this monorepo. "external/angular/packages/core/src", diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index f93c908f68..489d74b390 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -165,11 +165,11 @@ export function compile({allDepsCompiledWithBazel = true, compilerOpts, tsHost, if (inputs) { fileLoader = new CachedFileLoader(fileCache); // Resolve the inputs to absolute paths to match TypeScript internals - const resolvedInputs: {[path: string]: string} = {}; + const resolvedInputs = new Map(); const inputKeys = Object.keys(inputs); for (let i = 0; i < inputKeys.length; i++) { const key = inputKeys[i]; - resolvedInputs[resolveNormalizedPath(key)] = inputs[key]; + resolvedInputs.set(resolveNormalizedPath(key), inputs[key]); } fileCache.updateCache(resolvedInputs); } else { diff --git a/packages/bazel/src/schematics/ng-add/index.ts b/packages/bazel/src/schematics/ng-add/index.ts index 9a77610a55..d38588b7c4 100755 --- a/packages/bazel/src/schematics/ng-add/index.ts +++ b/packages/bazel/src/schematics/ng-add/index.ts @@ -48,10 +48,10 @@ function addDevDependenciesToPackageJson(options: Schema) { const devDependencies: {[k: string]: string} = { '@angular/bazel': angularCoreVersion, - '@bazel/bazel': '^0.25.1', + '@bazel/bazel': '^0.26.0-rc.5', '@bazel/ibazel': '^0.10.2', - '@bazel/karma': '0.27.12', - '@bazel/typescript': '0.27.12', + '@bazel/karma': '0.29.0', + '@bazel/typescript': '0.29.0', }; const recorder = host.beginUpdate(packageJson); @@ -174,7 +174,10 @@ function updateAngularJsonToUseBazelBuilder(options: Schema): Rule { replacePropertyInAstObject( recorder, architect, 'test', { builder: '@angular/bazel:build', - options: {'bazelCommand': 'test', 'targetLabel': '//src/...'}, + options: { + bazelCommand: 'test', + targetLabel: '//src:test', + }, }, indent); } diff --git a/packages/bazel/src/schematics/ng-add/index_spec.ts b/packages/bazel/src/schematics/ng-add/index_spec.ts index bac2d182ba..5dae5973da 100644 --- a/packages/bazel/src/schematics/ng-add/index_spec.ts +++ b/packages/bazel/src/schematics/ng-add/index_spec.ts @@ -61,22 +61,38 @@ describe('ng-add schematic', () => { new SchematicTestRunner('@angular/bazel', require.resolve('../collection.json')); }); - it('throws if package.json is not found', () => { + it('throws if package.json is not found', async() => { expect(host.files).toContain('/package.json'); host.delete('/package.json'); - expect(() => schematicRunner.runSchematic('ng-add', defaultOptions)) - .toThrowError('Could not find package.json'); + + let message = 'No error'; + + try { + await schematicRunner.runSchematicAsync('ng-add', defaultOptions).toPromise(); + } catch (e) { + message = e.message; + } + + expect(message).toBe('Could not find package.json'); }); - it('throws if angular.json is not found', () => { + it('throws if angular.json is not found', async() => { expect(host.files).toContain('/angular.json'); host.delete('/angular.json'); - expect(() => schematicRunner.runSchematic('ng-add', defaultOptions, host)) - .toThrowError('Could not find angular.json'); + + let message = 'No error'; + + try { + await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + } catch (e) { + message = e.message; + } + + expect(message).toBe('Could not find angular.json'); }); - it('should add @angular/bazel to package.json dependencies', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should add @angular/bazel to package.json dependencies', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).toContain('/package.json'); const content = host.readContent('/package.json'); @@ -91,8 +107,8 @@ describe('ng-add schematic', () => { expect(json.dependencies[core]).toBe(json.devDependencies[bazel]); }); - it('should add @bazel/* dev dependencies', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should add @bazel/* dev dependencies', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const content = host.readContent('/package.json'); const json = JSON.parse(content); const devDeps = Object.keys(json.devDependencies); @@ -101,12 +117,12 @@ describe('ng-add schematic', () => { expect(devDeps).toContain('@bazel/karma'); }); - it('should replace an existing dev dependency', () => { + it('should replace an existing dev dependency', async() => { expect(host.files).toContain('/package.json'); const packageJson = JSON.parse(host.readContent('/package.json')); packageJson.devDependencies['@angular/bazel'] = '4.2.42'; host.overwrite('/package.json', JSON.stringify(packageJson)); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const content = host.readContent('/package.json'); // It is possible that a dep gets added twice if the package already exists. expect(content.match(/@angular\/bazel/g) !.length).toEqual(1); @@ -114,29 +130,29 @@ describe('ng-add schematic', () => { expect(json.devDependencies['@angular/bazel']).toBe('1.2.3'); }); - it('should remove an existing dependency', () => { + it('should remove an existing dependency', async() => { expect(host.files).toContain('/package.json'); const packageJson = JSON.parse(host.readContent('/package.json')); packageJson.dependencies['@angular/bazel'] = '4.2.42'; expect(Object.keys(packageJson.dependencies)).toContain('@angular/bazel'); host.overwrite('/package.json', JSON.stringify(packageJson)); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const content = host.readContent('/package.json'); const json = JSON.parse(content); expect(Object.keys(json.dependencies)).not.toContain('@angular/bazel'); expect(json.devDependencies['@angular/bazel']).toBe('1.2.3'); }); - it('should not create Bazel workspace file', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should not create Bazel workspace file', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).not.toContain('/WORKSPACE'); expect(files).not.toContain('/BUILD.bazel'); }); - it('should produce main.dev.ts and main.prod.ts for AOT', () => { + it('should produce main.dev.ts and main.prod.ts for AOT', async() => { host.create('/src/main.ts', 'generated by CLI'); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; // main.dev.ts and main.prod.ts are used by Bazel for AOT expect(files).toContain('/src/main.dev.ts'); @@ -146,9 +162,9 @@ describe('ng-add schematic', () => { expect(files).toContain('/src/main.ts'); }); - it('should not overwrite index.html with script tags', () => { + it('should not overwrite index.html with script tags', async() => { host.create('/src/index.html', 'Hello World'); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).toContain('/src/index.html'); const content = host.readContent('/src/index.html'); @@ -156,34 +172,34 @@ describe('ng-add schematic', () => { expect(content).not.toMatch(''); }); - it('should generate main.dev.ts and main.prod.ts', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should generate main.dev.ts and main.prod.ts', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).toContain('/src/main.dev.ts'); expect(files).toContain('/src/main.prod.ts'); }); - it('should overwrite .gitignore for bazel-out directory', () => { + it('should overwrite .gitignore for bazel-out directory', async() => { host.create('.gitignore', '\n# compiled output\n'); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).toContain('/.gitignore'); const content = host.readContent('/.gitignore'); expect(content).toMatch('\n# compiled output\n/bazel-out\n'); }); - it('should create a backup for original angular.json', () => { + it('should create a backup for original angular.json', async() => { expect(host.files).toContain('/angular.json'); const original = host.readContent('/angular.json'); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); expect(host.files).toContain('/angular.json.bak'); const content = host.readContent('/angular.json.bak'); expect(content.startsWith('// This is a backup file')).toBe(true); expect(content).toMatch(original); }); - it('should update angular.json to use Bazel builder', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should update angular.json to use Bazel builder', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); const {files} = host; expect(files).toContain('/angular.json'); const content = host.readContent('/angular.json'); @@ -203,27 +219,27 @@ describe('ng-add schematic', () => { expect(lint.builder).toBe('@angular-devkit/build-angular:tslint'); }); - it('should get defaultProject if name is not provided', () => { + it('should get defaultProject if name is not provided', async() => { const options = {}; - host = schematicRunner.runSchematic('ng-add', options, host); + host = await schematicRunner.runSchematicAsync('ng-add', options, host).toPromise(); const content = host.readContent('/angular.json'); const json = JSON.parse(content); const builder = json.projects.demo.architect.build.builder; expect(builder).toBe('@angular/bazel:build'); }); - it('should create a backup for original tsconfig.json', () => { + it('should create a backup for original tsconfig.json', async() => { expect(host.files).toContain('/tsconfig.json'); const original = host.readContent('/tsconfig.json'); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); expect(host.files).toContain('/tsconfig.json.bak'); const content = host.readContent('/tsconfig.json.bak'); expect(content.startsWith('// This is a backup file')).toBe(true); expect(content).toMatch(original); }); - it('should remove Bazel-controlled options from tsconfig.json', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should remove Bazel-controlled options from tsconfig.json', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); expect(host.files).toContain('/tsconfig.json'); const content = host.readContent('/tsconfig.json'); expect(() => JSON.parse(content)).not.toThrow(); @@ -251,7 +267,7 @@ describe('ng-add schematic', () => { ['~7.0.1', false], ]; for (const [version, upgrade] of cases) { - it(`should ${upgrade ? '' : 'not '}upgrade v${version}')`, () => { + it(`should ${upgrade ? '' : 'not '}upgrade v${version}')`, async() => { host.overwrite('package.json', JSON.stringify({ name: 'demo', dependencies: { @@ -262,7 +278,7 @@ describe('ng-add schematic', () => { 'typescript': '3.2.2', }, })); - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); expect(host.files).toContain('/package.json'); const content = host.readContent('/package.json'); const json = JSON.parse(content); @@ -275,15 +291,15 @@ describe('ng-add schematic', () => { } }); - it('should add a postinstall step to package.json', () => { - host = schematicRunner.runSchematic('ng-add', defaultOptions, host); + it('should add a postinstall step to package.json', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); expect(host.files).toContain('/package.json'); const content = host.readContent('/package.json'); const json = JSON.parse(content); expect(json.scripts.postinstall).toBe('ngc -p ./angular-metadata.tsconfig.json'); }); - it('should work when run on a minimal project (without test and e2e targets)', () => { + it('should work when run on a minimal project (without test and e2e targets)', async() => { host.overwrite('angular.json', JSON.stringify({ projects: { 'demo': { @@ -298,7 +314,15 @@ describe('ng-add schematic', () => { }, })); - expect(() => schematicRunner.runSchematic('ng-add', defaultOptions, host)).not.toThrowError(); + let error: Error|null = null; + + try { + await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + } catch (e) { + error = e; + } + + expect(error).toBeNull(); }); }); diff --git a/packages/bazel/src/schematics/ng-new/index_spec.ts b/packages/bazel/src/schematics/ng-new/index_spec.ts index 3c33b3fb95..3d1fef16bf 100644 --- a/packages/bazel/src/schematics/ng-new/index_spec.ts +++ b/packages/bazel/src/schematics/ng-new/index_spec.ts @@ -16,18 +16,18 @@ describe('ng-new schematic', () => { version: '7.0.0', }; - it('should call external @schematics/angular', () => { + it('should call external @schematics/angular', async() => { const options = {...defaultOptions}; - const host = schematicRunner.runSchematic('ng-new', options); + const host = await schematicRunner.runSchematicAsync('ng-new', options).toPromise(); const {files} = host; // External schematic should produce workspace file angular.json expect(files).toContain('/demo/angular.json'); expect(files).toContain('/demo/package.json'); }); - it('should call ng-add to generate additional files needed by Bazel', () => { + it('should call ng-add to generate additional files needed by Bazel', async() => { const options = {...defaultOptions}; - const host = schematicRunner.runSchematic('ng-new', options); + const host = await schematicRunner.runSchematicAsync('ng-new', options).toPromise(); const {files} = host; expect(files).toContain('/demo/src/main.dev.ts'); expect(files).toContain('/demo/src/main.prod.ts'); diff --git a/packages/common/src/directives/ng_if.ts b/packages/common/src/directives/ng_if.ts index 4d9f63ddce..95a54972f0 100644 --- a/packages/common/src/directives/ng_if.ts +++ b/packages/common/src/directives/ng_if.ts @@ -219,12 +219,12 @@ export class NgIf { /** * Assert the correct type of the expression bound to the `ngIf` input within the template. * - * The presence of this method is a signal to the Ivy template type check compiler that when the - * `NgIf` structural directive renders its template, the type of the expression bound to `ngIf` - * should be narrowed in some way. For `NgIf`, it is narrowed to be non-null, which allows the - * strictNullChecks feature of TypeScript to work with `NgIf`. + * The presence of this static field is a signal to the Ivy template type check compiler that + * when the `NgIf` structural directive renders its template, the type of the expression bound + * to `ngIf` should be narrowed in some way. For `NgIf`, the binding expression itself is used to + * narrow its type, which allows the strictNullChecks feature of TypeScript to work with `NgIf`. */ - static ngTemplateGuard_ngIf(dir: NgIf, expr: E): expr is NonNullable { return true; } + static ngTemplateGuard_ngIf: 'binding'; } /** diff --git a/packages/common/src/directives/ng_switch.ts b/packages/common/src/directives/ng_switch.ts index 83d20067d9..11c0fc8aef 100644 --- a/packages/common/src/directives/ng_switch.ts +++ b/packages/common/src/directives/ng_switch.ts @@ -98,7 +98,7 @@ export class SwitchView { * @publicApi * @see `NgSwitchCase` * @see `NgSwitchDefault` - * @see [Stuctural Directives](guide/structural-directives) + * @see [Structural Directives](guide/structural-directives) * */ @Directive({selector: '[ngSwitch]'}) diff --git a/packages/common/src/i18n/locale_data.ts b/packages/common/src/i18n/locale_data.ts index 1d3301999a..975dcc0b69 100644 --- a/packages/common/src/i18n/locale_data.ts +++ b/packages/common/src/i18n/locale_data.ts @@ -6,10 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -/** - * @publicApi - */ -export const LOCALE_DATA: {[localeId: string]: any} = {}; +import {ɵLOCALE_DATA as LOCALE_DATA, ɵLocaleDataIndex as LocaleDataIndex} from '@angular/core'; /** * Register global data to be used internally by Angular. See the @@ -33,32 +30,6 @@ export function registerLocaleData(data: any, localeId?: string | any, extraData } } -/** - * Index of each type of locale data from the locale data array - */ -export const enum LocaleDataIndex { - LocaleId = 0, - DayPeriodsFormat, - DayPeriodsStandalone, - DaysFormat, - DaysStandalone, - MonthsFormat, - MonthsStandalone, - Eras, - FirstDayOfWeek, - WeekendRange, - DateFormat, - TimeFormat, - DateTimeFormat, - NumberSymbols, - NumberFormats, - CurrencySymbol, - CurrencyName, - Currencies, - PluralCase, - ExtraData -} - /** * Index of each type of locale data from the extra locale data array */ diff --git a/packages/common/src/i18n/locale_data_api.ts b/packages/common/src/i18n/locale_data_api.ts index 46d02421cc..072add5903 100644 --- a/packages/common/src/i18n/locale_data_api.ts +++ b/packages/common/src/i18n/locale_data_api.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import localeEn from './locale_en'; -import {LOCALE_DATA, LocaleDataIndex, ExtraLocaleDataIndex, CurrencyIndex} from './locale_data'; +import {ɵLocaleDataIndex as LocaleDataIndex, ɵfindLocaleData as findLocaleData, ɵgetLocalePluralCase} from '@angular/core'; import {CURRENCIES_EN, CurrenciesSymbols} from './currencies'; +import {CurrencyIndex, ExtraLocaleDataIndex} from './locale_data'; /** * Format styles that can be used to represent numbers. @@ -31,7 +31,8 @@ export enum NumberFormatStyle { * @see `NgPluralCase` * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) * - * @publicApi */ + * @publicApi + */ export enum Plural { Zero = 0, One = 1, @@ -485,19 +486,11 @@ function getLocaleCurrencies(locale: string): {[code: string]: CurrenciesSymbols } /** - * Retrieves the plural function used by ICU expressions to determine the plural case to use - * for a given locale. - * @param locale A locale code for the locale format rules to use. - * @returns The plural function for the locale. - * @see `NgPlural` - * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) - * + * @alias core/ɵgetLocalePluralCase * @publicApi */ -export function getLocalePluralCase(locale: string): (value: number) => Plural { - const data = findLocaleData(locale); - return data[LocaleDataIndex.PluralCase]; -} +export const getLocalePluralCase: (locale: string) => ((value: number) => Plural) = + ɵgetLocalePluralCase; function checkFullData(data: any) { if (!data[LocaleDataIndex.ExtraData]) { @@ -609,37 +602,7 @@ function extractTime(time: string): Time { return {hours: +h, minutes: +m}; } -/** - * Finds the locale data for a given locale. - * - * @param locale The locale code. - * @returns The locale data. - * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) - * - * @publicApi - */ -export function findLocaleData(locale: string): any { - const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); - let match = LOCALE_DATA[normalizedLocale]; - if (match) { - return match; - } - - // let's try to find a parent locale - const parentLocale = normalizedLocale.split('-')[0]; - match = LOCALE_DATA[parentLocale]; - - if (match) { - return match; - } - - if (parentLocale === 'en') { - return localeEn; - } - - throw new Error(`Missing locale data for the locale "${locale}".`); -} /** * Retrieves the currency symbol for a given currency code. diff --git a/packages/common/src/pipes/slice_pipe.ts b/packages/common/src/pipes/slice_pipe.ts index e27fa541da..359164957e 100644 --- a/packages/common/src/pipes/slice_pipe.ts +++ b/packages/common/src/pipes/slice_pipe.ts @@ -107,6 +107,10 @@ export class SlicePipe implements PipeTransform { * * **如果为负数**:从列表或字符串中返回 `end` 索引之前的所有条目。 */ + transform(value: ReadonlyArray, start: number, end?: number): Array; + transform(value: string, start: number, end?: number): string; + transform(value: null, start: number, end?: number): null; + transform(value: undefined, start: number, end?: number): undefined; transform(value: any, start: number, end?: number): any { if (value == null) return value; diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index bc8ae923df..bc25a7b83d 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -11,7 +11,6 @@ import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_out import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed, async} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {modifiedInIvy} from '@angular/private/testing'; describe('insert/remove', () => { @@ -108,19 +107,18 @@ describe('insert/remove', () => { })); - modifiedInIvy('Static ViewChild and ContentChild queries are resolved in update mode') - .it('should resolve with an injector', async(() => { - let fixture = TestBed.createComponent(TestComponent); + it('should resolve with an injector', async(() => { + let fixture = TestBed.createComponent(TestComponent); - // We are accessing a ViewChild (ngComponentOutlet) before change detection has run - fixture.componentInstance.cmpRef = null; - fixture.componentInstance.currentComponent = InjectedComponent; - fixture.detectChanges(); - let cmpRef: ComponentRef = fixture.componentInstance.cmpRef !; - expect(cmpRef).toBeAnInstanceOf(ComponentRef); - expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); - expect(cmpRef.instance.testToken).toBeNull(); - })); + // We are accessing a ViewChild (ngComponentOutlet) before change detection has run + fixture.componentInstance.cmpRef = null; + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.detectChanges(); + let cmpRef: ComponentRef = fixture.componentInstance.cmpRef !; + expect(cmpRef).toBeAnInstanceOf(ComponentRef); + expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); + expect(cmpRef.instance.testToken).toBeNull(); + })); it('should render projectable nodes, if supplied', async(() => { const template = `projected foo${TEST_CMP_TEMPLATE}`; @@ -240,7 +238,7 @@ class TestComponent { // TODO(issue/24571): remove '!'. @ViewChildren(TemplateRef) tplRefs !: QueryList>; // TODO(issue/24571): remove '!'. - @ViewChild(NgComponentOutlet) ngComponentOutlet !: NgComponentOutlet; + @ViewChild(NgComponentOutlet, {static: true}) ngComponentOutlet !: NgComponentOutlet; constructor(public vcRef: ViewContainerRef) {} } diff --git a/packages/common/test/directives/ng_switch_spec.ts b/packages/common/test/directives/ng_switch_spec.ts index f7042859e5..436d8ad36b 100644 --- a/packages/common/test/directives/ng_switch_spec.ts +++ b/packages/common/test/directives/ng_switch_spec.ts @@ -223,8 +223,8 @@ class TestComponent { ` }) class ComplexComponent { - @ViewChild('foo') foo !: TemplateRef; - @ViewChild('bar') bar !: TemplateRef; + @ViewChild('foo', {static: true}) foo !: TemplateRef; + @ViewChild('bar', {static: true}) bar !: TemplateRef; state: string = 'case1'; } diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts index 9fee387070..6babf0bd20 100644 --- a/packages/common/test/i18n/locale_data_api_spec.ts +++ b/packages/common/test/i18n/locale_data_api_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ɵfindLocaleData as findLocaleData} from '@angular/core'; import localeCaESVALENCIA from '@angular/common/locales/ca-ES-VALENCIA'; import localeEn from '@angular/common/locales/en'; import localeFr from '@angular/common/locales/fr'; @@ -13,7 +14,7 @@ import localeZh from '@angular/common/locales/zh'; import localeFrCA from '@angular/common/locales/fr-CA'; import localeEnAU from '@angular/common/locales/en-AU'; import {registerLocaleData} from '../../src/i18n/locale_data'; -import {findLocaleData, getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api'; +import {getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api'; { describe('locale data api', () => { diff --git a/packages/common/test/pipes/slice_pipe_spec.ts b/packages/common/test/pipes/slice_pipe_spec.ts index 8ed5c41bb2..63c79f1880 100644 --- a/packages/common/test/pipes/slice_pipe_spec.ts +++ b/packages/common/test/pipes/slice_pipe_spec.ts @@ -26,9 +26,13 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; describe('supports', () => { it('should support strings', () => { expect(() => pipe.transform(str, 0)).not.toThrow(); }); it('should support lists', () => { expect(() => pipe.transform(list, 0)).not.toThrow(); }); + it('should support readonly lists', + () => { expect(() => pipe.transform(list as ReadonlyArray, 0)).not.toThrow(); }); it('should not support other objects', - () => { expect(() => pipe.transform({}, 0)).toThrow(); }); + // this would not compile + // so we cast as `any` to check that it throws for unsupported objects + () => { expect(() => pipe.transform({} as any, 0)).toThrow(); }); }); describe('transform', () => { @@ -36,6 +40,9 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; it('should return null if the value is null', () => { expect(pipe.transform(null, 1)).toBe(null); }); + it('should return undefined if the value is undefined', + () => { expect(pipe.transform(undefined, 1)).toBe(undefined); }); + it('should return all items after START index when START is positive and END is omitted', () => { expect(pipe.transform(list, 3)).toEqual([4, 5]); diff --git a/packages/common/upgrade/src/location_shim.ts b/packages/common/upgrade/src/location_shim.ts index 49eb8d4c4b..b9e7cda766 100644 --- a/packages/common/upgrade/src/location_shim.ts +++ b/packages/common/upgrade/src/location_shim.ts @@ -22,7 +22,10 @@ const DEFAULT_PORTS: {[key: string]: number} = { }; /** - * Docs TBD. + * Location service that provides a drop-in replacement for the $location service + * provided in AngularJS. + * + * @see [Using the Angular Unified Location Service](guide/upgrade#using-the-unified-angular-location-service) * * @publicApi */ @@ -39,9 +42,16 @@ export class $locationShim { private $$search: any = ''; private $$hash: string = ''; private $$state: unknown; + private $$changeListeners: [ + ((url: string, state: unknown, oldUrl: string, oldState: unknown, err?: (e: Error) => void) => + void), + (e: Error) => void + ][] = []; private cachedState: unknown = null; + + constructor( $injector: any, private location: Location, private platformLocation: PlatformLocation, private urlCodec: UrlCodec, private locationStrategy: LocationStrategy) { @@ -313,6 +323,42 @@ export class $locationShim { } } + /** + * Registers listeners for URL changes. This API is used to catch updates performed by the + * AngularJS framework. These changes are a subset of the `$locationChangeStart` and + * `$locationChangeSuccess` events which fire when AngularJS updates its internally-referenced + * version of the browser URL. + * + * It's possible for `$locationChange` events to happen, but for the browser URL + * (window.location) to remain unchanged. This `onChange` callback will fire only when AngularJS + * actually updates the browser URL (window.location). + * + * @param fn The callback function that is triggered for the listener when the URL changes. + * @param err The callback function that is triggered when an error occurs. + */ + onChange( + fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void, + err: (e: Error) => void = (e: Error) => {}) { + this.$$changeListeners.push([fn, err]); + } + + /** @internal */ + $$notifyChangeListeners( + url: string = '', state: unknown, oldUrl: string = '', oldState: unknown) { + this.$$changeListeners.forEach(([fn, err]) => { + try { + fn(url, state, oldUrl, oldState); + } catch (e) { + err(e); + } + }); + } + + /** + * Parses the provided URL, and sets the current URL to the parsed result. + * + * @param url The URL string. + */ $$parse(url: string) { let pathUrl: string|undefined; if (url.startsWith('/')) { @@ -333,6 +379,12 @@ export class $locationShim { this.composeUrls(); } + /** + * Parses the provided URL and its relative URL. + * + * @param url The full URL string. + * @param relHref A URL string relative to the full URL string. + */ $$parseLinkUrl(url: string, relHref?: string|null): boolean { // When relHref is passed, it should be a hash and is handled separately if (relHref && relHref[0] === '#') { @@ -363,6 +415,7 @@ export class $locationShim { // state object; this makes possible quick checking if the state changed in the digest // loop. Checking deep equality would be too expensive. this.$$state = this.browserState(); + this.$$notifyChangeListeners(url, state, oldUrl, oldState); } catch (e) { // Restore old values if pushState fails this.url(oldUrl); @@ -379,9 +432,8 @@ export class $locationShim { } /** - * This method is getter only. - * - * Return full URL representation with all segments encoded according to rules specified in + * Retrieves the full URL representation with all segments encoded according to + * rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * @@ -394,12 +446,8 @@ export class $locationShim { absUrl(): string { return this.$$absUrl; } /** - * This method is getter / setter. - * - * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. - * - * Change path, search and hash, when called with parameter and return `$location`. - * + * Retrieves the current URL, or sets a new URL. When setting a URL, + * changes the path, search, and hash, and returns a reference to its own instance. * * ```js * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo @@ -429,10 +477,7 @@ export class $locationShim { } /** - * This method is getter only. - * - * Return protocol of current URL. - * + * Retrieves the protocol of the current URL. * * ```js * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo @@ -443,11 +488,9 @@ export class $locationShim { protocol(): string { return this.$$protocol; } /** - * This method is getter only. + * Retrieves the protocol of the current URL. * - * Return host of current URL. - * - * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this + * In contrast to the non-AngularJS version `location.host` which returns `hostname:port`, this * returns the `hostname` portion only. * * @@ -466,10 +509,7 @@ export class $locationShim { host(): string { return this.$$host; } /** - * This method is getter only. - * - * Return port of current URL. - * + * Retrieves the port of the current URL. * * ```js * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo @@ -480,16 +520,12 @@ export class $locationShim { port(): number|null { return this.$$port; } /** - * This method is getter / setter. + * Retrieves the path of the current URL, or changes the path and returns a reference to its own + * instance. * - * Return path of current URL when called without any parameter. - * - * Change path when called with parameter and return `$location`. - * - * Note: Path should always begin with forward slash (/), this method will add the forward slash + * Paths should always begin with forward slash (/). This method adds the forward slash * if it is missing. * - * * ```js * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * let path = $location.path(); @@ -514,11 +550,8 @@ export class $locationShim { } /** - * This method is getter / setter. - * - * Return search part (as object) of current URL when called without any parameter. - * - * Change search part when called with parameter and return `$location`. + * Retrieves a map of the search parameters of the current URL, or changes a search + * part and returns a reference to its own instance. * * * ```js @@ -551,8 +584,7 @@ export class $locationShim { * If `paramValue` is `true`, the property specified via the first argument will be added with no * value nor trailing equal sign. * - * @return {Object} If called with no arguments returns the parsed `search` object. If called with - * one or more arguments returns `$location` object itself. + * @return {Object} The parsed `search` object of the current URL, or the changed `search` object. */ search(): {[key: string]: unknown}; search(search: string|number|{[key: string]: unknown}): this; @@ -599,12 +631,8 @@ export class $locationShim { } /** - * This method is getter / setter. - * - * Returns the hash fragment when called without any parameters. - * - * Changes the hash fragment when called with a parameter and returns `$location`. - * + * Retrieves the current hash fragment, or changes the hash fragment and returns a reference to + * its own instance. * * ```js * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue @@ -626,7 +654,7 @@ export class $locationShim { } /** - * If called, all changes to $location during the current `$digest` will replace the current + * Changes to `$location` during the current `$digest` will replace the current * history record, instead of adding a new one. */ replace(): this { @@ -635,15 +663,13 @@ export class $locationShim { } /** - * This method is getter / setter. - * - * Return the history state object when called without any parameter. + * Retrieves the history state object when called without any parameter. * * Change the history state object when called with one parameter and return `$location`. * The state object is later passed to `pushState` or `replaceState`. * - * NOTE: This method is supported only in HTML5 mode and only in browsers supporting - * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API methods such as `pushState` and `replaceState`. If you need to support * older browsers (like IE9 or Android < 4.0), don't use this method. * */ @@ -660,7 +686,8 @@ export class $locationShim { } /** - * Docs TBD. + * The factory function used to create an instance of the `$locationShim` in Angular, + * and provides an API-compatiable `$locationProvider` for AngularJS. * * @publicApi */ @@ -670,6 +697,9 @@ export class $locationShimProvider { private platformLocation: PlatformLocation, private urlCodec: UrlCodec, private locationStrategy: LocationStrategy) {} + /** + * Factory method that returns an instance of the $locationShim + */ $get() { return new $locationShim( this.ngUpgrade.$injector, this.location, this.platformLocation, this.urlCodec, diff --git a/packages/common/upgrade/src/location_upgrade_module.ts b/packages/common/upgrade/src/location_upgrade_module.ts index 4cd9ee35b4..720dd986a0 100644 --- a/packages/common/upgrade/src/location_upgrade_module.ts +++ b/packages/common/upgrade/src/location_upgrade_module.ts @@ -20,15 +20,31 @@ import {AngularJSUrlCodec, UrlCodec} from './params'; * @publicApi */ export interface LocationUpgradeConfig { + /** + * Configures whether the location upgrade module should use the `HashLocationStrategy` + * or the `PathLocationStrategy` + */ useHash?: boolean; + /** + * Configures the hash prefix used in the URL when using the `HashLocationStrategy` + */ hashPrefix?: string; + /** + * Configures the URL codec for encoding and decoding URLs. Default is the `AngularJSCodec` + */ urlCodec?: typeof UrlCodec; + /** + * Configures the base href when used in server-side rendered applications + */ serverBaseHref?: string; + /** + * Configures the base href when used in client-side rendered applications + */ appBaseHref?: string; } /** - * Is used in DI to configure the location upgrade package. + * A provider token used to configure the location upgrade module. * * @publicApi */ @@ -38,7 +54,9 @@ export const LOCATION_UPGRADE_CONFIGURATION = const APP_BASE_HREF_RESOLVED = new InjectionToken('APP_BASE_HREF_RESOLVED'); /** - * Module used for configuring Angular's LocationUpgradeService. + * `NgModule` used for providing and configuring Angular's Unified Location Service for upgrading. + * + * @see [Using the Unified Angular Location Service](guide/upgrade#using-the-unified-angular-location-service) * * @publicApi */ diff --git a/packages/common/upgrade/src/params.ts b/packages/common/upgrade/src/params.ts index a357c17a51..025c2edcaa 100644 --- a/packages/common/upgrade/src/params.ts +++ b/packages/common/upgrade/src/params.ts @@ -12,21 +12,80 @@ * @publicApi **/ export abstract class UrlCodec { + /** + * Encodes the path from the provided string + * + * @param path The path string + */ abstract encodePath(path: string): string; + + /** + * Decodes the path from the provided string + * + * @param path The path string + */ abstract decodePath(path: string): string; + /** + * Encodes the search string from the provided string or object + * + * @param path The path string or object + */ abstract encodeSearch(search: string|{[k: string]: unknown}): string; + + /** + * Decodes the search objects from the provided string + * + * @param path The path string + */ abstract decodeSearch(search: string): {[k: string]: unknown}; + /** + * Encodes the hash from the provided string + * + * @param path The hash string + */ abstract encodeHash(hash: string): string; + + /** + * Decodes the hash from the provided string + * + * @param path The hash string + */ abstract decodeHash(hash: string): string; + /** + * Normalizes the URL from the provided string + * + * @param path The URL string + */ abstract normalize(href: string): string; + + + /** + * Normalizes the URL from the provided string, search, hash, and base URL parameters + * + * @param path The URL path + * @param search The search object + * @param hash The has string + * @param baseUrl The base URL for the URL + */ abstract normalize(path: string, search: {[k: string]: unknown}, hash: string, baseUrl?: string): string; - abstract areEqual(a: string, b: string): boolean; + /** + * Checks whether the two strings are equal + * @param valA First string for comparison + * @param valB Second string for comparison + */ + abstract areEqual(valA: string, valB: string): boolean; + /** + * Parses the URL string based on the base URL + * + * @param url The full URL string + * @param base The base for the URL + */ abstract parse(url: string, base?: string): { href: string, protocol: string, @@ -40,8 +99,8 @@ export abstract class UrlCodec { } /** - * A `AngularJSUrlCodec` that uses logic from AngularJS to serialize and parse URLs - * and URL parameters + * A `UrlCodec` that uses logic from AngularJS to serialize and parse URLs + * and URL parameters. * * @publicApi */ @@ -134,7 +193,7 @@ export class AngularJSUrlCodec implements UrlCodec { } } - areEqual(a: string, b: string) { return this.normalize(a) === this.normalize(b); } + areEqual(valA: string, valB: string) { return this.normalize(valA) === this.normalize(valB); } // https://github.com/angular/angular.js/blob/864c7f0/src/ng/urlUtils.js#L60 parse(url: string, base?: string) { diff --git a/packages/common/upgrade/test/upgrade.spec.ts b/packages/common/upgrade/test/upgrade.spec.ts index 72b476eb9c..14d422fa2b 100644 --- a/packages/common/upgrade/test/upgrade.spec.ts +++ b/packages/common/upgrade/test/upgrade.spec.ts @@ -624,6 +624,87 @@ describe('New URL Parsing', () => { }); }); +describe('$location.onChange()', () => { + + let $location: $locationShim; + let upgradeModule: UpgradeModule; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + LocationUpgradeTestModule.config({useHash: false, startUrl: 'http://host.com/'}), + ], + providers: [UpgradeModule], + }); + + upgradeModule = TestBed.get(UpgradeModule); + upgradeModule.$injector = {get: injectorFactory()}; + }); + + beforeEach(inject([$locationShim], (loc: $locationShim) => { $location = loc; })); + + it('should have onChange method', () => { expect(typeof $location.onChange).toBe('function'); }); + + it('should add registered functions to changeListeners', () => { + + function changeListener(url: string, state: unknown) { return undefined; } + function errorHandler(e: Error) {} + + expect(($location as any).$$changeListeners.length).toBe(0); + + $location.onChange(changeListener, errorHandler); + + expect(($location as any).$$changeListeners.length).toBe(1); + expect(($location as any).$$changeListeners[0][0]).toEqual(changeListener); + expect(($location as any).$$changeListeners[0][1]).toEqual(errorHandler); + }); + + it('should call changeListeners when URL is updated', () => { + + const onChangeVals = + {url: 'url', state: 'state' as unknown, oldUrl: 'oldUrl', oldState: 'oldState' as unknown}; + + function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) { + onChangeVals.url = url; + onChangeVals.state = state; + onChangeVals.oldUrl = oldUrl; + onChangeVals.oldState = oldState; + } + + $location.onChange(changeListener); + + // Mock out setting browserUrl + ($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {}; + + const newState = {foo: 'bar'}; + ($location as any).setBrowserUrlWithFallback('/newUrl', false, newState); + expect(onChangeVals.url).toBe('/newUrl'); + expect(onChangeVals.state).toBe(newState); + expect(onChangeVals.oldUrl).toBe('/'); + expect(onChangeVals.oldState).toBe(null); + }); + + it('should call forward errors to error handler', () => { + + let error !: Error; + + function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) { + throw new Error('Handle error'); + } + function errorHandler(e: Error) { error = e; } + + $location.onChange(changeListener, errorHandler); + + // Mock out setting browserUrl + ($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {}; + + ($location as any).setBrowserUrlWithFallback('/newUrl'); + expect(error.message).toBe('Handle error'); + }); + +}); + function parseLinkAndReturn(location: $locationShim, toUrl: string, relHref?: string) { const resetUrl = location.$$parseLinkUrl(toUrl, relHref); return resetUrl && location.absUrl() || undefined; diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 4ceb6afa7b..2c1eab6262 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -43,6 +43,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/util", "@npm//@bazel/typescript", "@npm//@types", + "@npm//reflect-metadata", "@npm//tsickle", "@npm//typescript", ], diff --git a/packages/compiler-cli/integrationtest/src/queries.ts b/packages/compiler-cli/integrationtest/src/queries.ts index 8b85bfd9fa..e3d9a0840e 100644 --- a/packages/compiler-cli/integrationtest/src/queries.ts +++ b/packages/compiler-cli/integrationtest/src/queries.ts @@ -15,7 +15,7 @@ export class CompForChildQuery { @Component( {selector: 'comp-with-child-query', template: ''}) export class CompWithChildQuery { - @ViewChild(CompForChildQuery) child: CompForChildQuery; + @ViewChild(CompForChildQuery, {static: true}) child: CompForChildQuery; @ViewChildren(CompForChildQuery) children: QueryList; } diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 1a24bfab42..602f8d4829 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -70,7 +70,8 @@ export class DecorationAnalyzer { fullMetaReader = new CompoundMetadataReader([this.metaRegistry, this.dtsMetaReader]); refEmitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host), + new AbsoluteModuleStrategy( + this.program, this.typeChecker, this.options, this.host, this.reflectionHost), // TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc // projects only ever have one rootDir. Instead, ngcc should just switch its emitted import // based on whether a bestGuessOwningModule is present in the Reference. diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index 11894f9d13..6de29258cf 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -63,7 +63,7 @@ export class ModuleWithProvidersAnalyzer { ngModule = {node: dtsNgModule, viaModule: null}; } const dtsFile = dtsFn.getSourceFile(); - const analysis = analyses.get(dtsFile) || []; + const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile) : []; analysis.push({declaration: dtsFn, ngModule}); analyses.set(dtsFile, analysis); } diff --git a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts index 72698b5582..8ca9d7ed3b 100644 --- a/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/private_declarations_analyzer.ts @@ -49,8 +49,8 @@ export class PrivateDeclarationsAnalyzer { if (exports) { exports.forEach((declaration, exportedName) => { if (hasNameIdentifier(declaration.node)) { - const privateDeclaration = privateDeclarations.get(declaration.node.name); - if (privateDeclaration) { + if (privateDeclarations.has(declaration.node.name)) { + const privateDeclaration = privateDeclarations.get(declaration.node.name) !; if (privateDeclaration.node !== declaration.node) { throw new Error(`${declaration.node.name.text} is declared multiple times.`); } @@ -96,7 +96,7 @@ export class PrivateDeclarationsAnalyzer { return Array.from(privateDeclarations.keys()).map(id => { const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile()); const declaration = privateDeclarations.get(id) !; - const alias = exportAliasDeclarations.get(id) || null; + const alias = exportAliasDeclarations.has(id) ? exportAliasDeclarations.get(id) ! : null; const dtsDeclaration = this.host.getDtsDeclaration(declaration.node); const dtsFrom = dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile()); diff --git a/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts new file mode 100644 index 0000000000..352844d282 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/dependencies/commonjs_dependency_host.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; +import {isRequireCall} from '../host/commonjs_host'; + +import {DependencyHost, DependencyInfo} from './dependency_host'; +import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; + +/** + * Helper functions for computing dependencies. + */ +export class CommonJsDependencyHost implements DependencyHost { + constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {} + + /** + * Find all the dependencies for the entry-point at the given path. + * + * @param entryPointPath The absolute path to the JavaScript file that represents an entry-point. + * @returns Information about the dependencies of the entry-point, including those that were + * missing or deep imports into other entry-points. + */ + findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo { + const dependencies = new Set(); + const missing = new Set(); + const deepImports = new Set(); + const alreadySeen = new Set(); + this.recursivelyFindDependencies( + entryPointPath, dependencies, missing, deepImports, alreadySeen); + return {dependencies, missing, deepImports}; + } + + /** + * Compute the dependencies of the given file. + * + * @param file An absolute path to the file whose dependencies we want to get. + * @param dependencies A set that will have the absolute paths of resolved entry points added to + * it. + * @param missing A set that will have the dependencies that could not be found added to it. + * @param deepImports A set that will have the import paths that exist but cannot be mapped to + * entry-points, i.e. deep-imports. + * @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck + * in a + * circular dependency loop. + */ + private recursivelyFindDependencies( + file: AbsoluteFsPath, dependencies: Set, missing: Set, + deepImports: Set, alreadySeen: Set): void { + const fromContents = this.fs.readFile(file); + if (!this.hasRequireCalls(fromContents)) { + // Avoid parsing the source file as there are no require calls. + return; + } + + // Parse the source into a TypeScript AST and then walk it looking for imports and re-exports. + const sf = + ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS); + + for (const statement of sf.statements) { + const declarations = + ts.isVariableStatement(statement) ? statement.declarationList.declarations : []; + for (const declaration of declarations) { + if (declaration.initializer && isRequireCall(declaration.initializer)) { + const importPath = declaration.initializer.arguments[0].text; + const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file); + if (resolvedModule) { + if (resolvedModule instanceof ResolvedRelativeModule) { + const internalDependency = resolvedModule.modulePath; + if (!alreadySeen.has(internalDependency)) { + alreadySeen.add(internalDependency); + this.recursivelyFindDependencies( + internalDependency, dependencies, missing, deepImports, alreadySeen); + } + } else { + if (resolvedModule instanceof ResolvedDeepImport) { + deepImports.add(resolvedModule.importPath); + } else { + dependencies.add(resolvedModule.entryPointPath); + } + } + } else { + missing.add(importPath); + } + } + } + } + } + + /** + * Check whether a source file needs to be parsed for imports. + * This is a performance short-circuit, which saves us from creating + * a TypeScript AST unnecessarily. + * + * @param source The content of the source file to check. + * + * @returns false if there are definitely no require calls + * in this file, true otherwise. + */ + hasRequireCalls(source: string): boolean { return /require\(['"]/.test(source); } +} diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts index b2210eb39c..c2a6924c6f 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; export interface DependencyHost { findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo; @@ -14,6 +14,6 @@ export interface DependencyHost { export interface DependencyInfo { dependencies: Set; - missing: Set; - deepImports: Set; + missing: Set; + deepImports: Set; } diff --git a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts index d6e344b5b2..092d1f65b8 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts @@ -7,15 +7,12 @@ */ import {DepGraph} from 'dependency-graph'; - import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; import {Logger} from '../logging/logger'; -import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; - +import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point'; import {DependencyHost} from './dependency_host'; - - /** * Holds information about entry points that are removed because * they have dependencies that are missing (directly or transitively). @@ -68,7 +65,9 @@ export interface SortedEntryPointsInfo extends DependencyDiagnostics { entryPoin * A class that resolves dependencies between entry-points. */ export class DependencyResolver { - constructor(private logger: Logger, private host: DependencyHost) {} + constructor( + private fs: FileSystem, private logger: Logger, + private hosts: Partial>) {} /** * Sort the array of entry points so that the dependant entry points always come later than * their dependencies in the array. @@ -118,8 +117,13 @@ export class DependencyResolver { // Now add the dependencies between them angularEntryPoints.forEach(entryPoint => { - const entryPointPath = getEntryPointPath(entryPoint); - const {dependencies, missing, deepImports} = this.host.findDependencies(entryPointPath); + const formatInfo = this.getEntryPointFormatInfo(entryPoint); + const host = this.hosts[formatInfo.format]; + if (!host) { + throw new Error( + `Could not find a suitable format for computing dependencies of entry-point: '${entryPoint.path}'.`); + } + const {dependencies, missing, deepImports} = host.findDependencies(formatInfo.path); if (missing.size > 0) { // This entry point has dependencies that are missing @@ -162,20 +166,22 @@ export class DependencyResolver { }); } } -} -function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath { - const properties = Object.keys(entryPoint.packageJson); - for (let i = 0; i < properties.length; i++) { - const property = properties[i] as EntryPointJsonProperty; - const format = getEntryPointFormat(property); + private getEntryPointFormatInfo(entryPoint: EntryPoint): + {format: EntryPointFormat, path: AbsoluteFsPath} { + const properties = Object.keys(entryPoint.packageJson); + for (let i = 0; i < properties.length; i++) { + const property = properties[i] as EntryPointJsonProperty; + const format = getEntryPointFormat(this.fs, entryPoint, property); - if (format === 'esm2015' || format === 'esm5') { - const formatPath = entryPoint.packageJson[property] !; - return AbsoluteFsPath.resolve(entryPoint.path, formatPath); + if (format === 'esm2015' || format === 'esm5' || format === 'umd' || format === 'commonjs') { + const formatPath = entryPoint.packageJson[property] !; + return {format, path: AbsoluteFsPath.resolve(entryPoint.path, formatPath)}; + } } + throw new Error( + `There is no appropriate source code format in '${entryPoint.path}' entry-point.`); } - throw new Error(`There is no format with import statements in '${entryPoint.path}' entry-point.`); } interface DependencyGraph extends DependencyDiagnostics { diff --git a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts index 33c7630698..0b164fac20 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; import {DependencyHost, DependencyInfo} from './dependency_host'; import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; @@ -28,8 +28,8 @@ export class EsmDependencyHost implements DependencyHost { */ findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo { const dependencies = new Set(); - const missing = new Set(); - const deepImports = new Set(); + const missing = new Set(); + const deepImports = new Set(); const alreadySeen = new Set(); this.recursivelyFindDependencies( entryPointPath, dependencies, missing, deepImports, alreadySeen); diff --git a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts index e8c12e361d..c558a2bb97 100644 --- a/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts +++ b/packages/compiler-cli/ngcc/src/dependencies/module_resolver.ts @@ -118,7 +118,7 @@ export class ModuleResolver { */ private resolveAsEntryPoint(moduleName: string, fromPath: AbsoluteFsPath): ResolvedModule|null { let folder = fromPath; - while (folder !== '/') { + while (!AbsoluteFsPath.isRoot(folder)) { folder = AbsoluteFsPath.dirname(folder); if (folder.endsWith('node_modules')) { // Skip up if the folder already ends in node_modules @@ -225,7 +225,7 @@ export class ModuleResolver { */ private findPackagePath(path: AbsoluteFsPath): AbsoluteFsPath|null { let folder = path; - while (folder !== '/') { + while (!AbsoluteFsPath.isRoot(folder)) { folder = AbsoluteFsPath.dirname(folder); if (this.fs.exists(AbsoluteFsPath.join(folder, 'package.json'))) { return folder; diff --git a/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts new file mode 100644 index 0000000000..d00790a494 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/dependencies/umd_dependency_host.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; +import {getImportsOfUmdModule, parseStatementForUmdModule} from '../host/umd_host'; + +import {DependencyHost, DependencyInfo} from './dependency_host'; +import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver'; + + + +/** + * Helper functions for computing dependencies. + */ +export class UmdDependencyHost implements DependencyHost { + constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {} + + /** + * Find all the dependencies for the entry-point at the given path. + * + * @param entryPointPath The absolute path to the JavaScript file that represents an entry-point. + * @returns Information about the dependencies of the entry-point, including those that were + * missing or deep imports into other entry-points. + */ + findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo { + const dependencies = new Set(); + const missing = new Set(); + const deepImports = new Set(); + const alreadySeen = new Set(); + this.recursivelyFindDependencies( + entryPointPath, dependencies, missing, deepImports, alreadySeen); + return {dependencies, missing, deepImports}; + } + + /** + * Compute the dependencies of the given file. + * + * @param file An absolute path to the file whose dependencies we want to get. + * @param dependencies A set that will have the absolute paths of resolved entry points added to + * it. + * @param missing A set that will have the dependencies that could not be found added to it. + * @param deepImports A set that will have the import paths that exist but cannot be mapped to + * entry-points, i.e. deep-imports. + * @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck + * in a + * circular dependency loop. + */ + private recursivelyFindDependencies( + file: AbsoluteFsPath, dependencies: Set, missing: Set, + deepImports: Set, alreadySeen: Set): void { + const fromContents = this.fs.readFile(file); + if (!this.hasRequireCalls(fromContents)) { + // Avoid parsing the source file as there are no require calls. + return; + } + + // Parse the source into a TypeScript AST and then walk it looking for imports and re-exports. + const sf = + ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS); + if (sf.statements.length !== 1) { + return; + } + + const umdModule = parseStatementForUmdModule(sf.statements[0]); + const umdImports = umdModule && getImportsOfUmdModule(umdModule); + if (umdImports === null) { + return; + } + + umdImports.forEach(umdImport => { + const resolvedModule = this.moduleResolver.resolveModuleImport(umdImport.path, file); + if (resolvedModule) { + if (resolvedModule instanceof ResolvedRelativeModule) { + const internalDependency = resolvedModule.modulePath; + if (!alreadySeen.has(internalDependency)) { + alreadySeen.add(internalDependency); + this.recursivelyFindDependencies( + internalDependency, dependencies, missing, deepImports, alreadySeen); + } + } else { + if (resolvedModule instanceof ResolvedDeepImport) { + deepImports.add(resolvedModule.importPath); + } else { + dependencies.add(resolvedModule.entryPointPath); + } + } + } else { + missing.add(umdImport.path); + } + }); + } + + /** + * Check whether a source file needs to be parsed for imports. + * This is a performance short-circuit, which saves us from creating + * a TypeScript AST unnecessarily. + * + * @param source The content of the source file to check. + * + * @returns false if there are definitely no require calls + * in this file, true otherwise. + */ + hasRequireCalls(source: string): boolean { return /require\(['"]/.test(source); } +} diff --git a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts index 7f821c3986..8f4e7c797c 100644 --- a/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts +++ b/packages/compiler-cli/ngcc/src/file_system/node_js_file_system.ts @@ -23,7 +23,7 @@ export class NodeJSFileSystem implements FileSystem { readdir(path: AbsoluteFsPath): PathSegment[] { return fs.readdirSync(path) as PathSegment[]; } lstat(path: AbsoluteFsPath): fs.Stats { return fs.lstatSync(path); } stat(path: AbsoluteFsPath): fs.Stats { return fs.statSync(path); } - pwd() { return AbsoluteFsPath.fromUnchecked(process.cwd()); } + pwd() { return AbsoluteFsPath.from(process.cwd()); } copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { cp(from, to); } moveFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { mv(from, to); } ensureDir(path: AbsoluteFsPath): void { mkdir('-p', path); } diff --git a/packages/compiler-cli/ngcc/src/host/commonjs_host.ts b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts new file mode 100644 index 0000000000..979f6604ea --- /dev/null +++ b/packages/compiler-cli/ngcc/src/host/commonjs_host.ts @@ -0,0 +1,186 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {Declaration, Import} from '../../../src/ngtsc/reflection'; +import {Logger} from '../logging/logger'; +import {BundleProgram} from '../packages/bundle_program'; +import {Esm5ReflectionHost} from './esm5_host'; + +export class CommonJsReflectionHost extends Esm5ReflectionHost { + protected commonJsExports = new Map|null>(); + constructor( + logger: Logger, isCore: boolean, protected program: ts.Program, + protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) { + super(logger, isCore, program.getTypeChecker(), dts); + } + + getImportOfIdentifier(id: ts.Identifier): Import|null { + const requireCall = this.findCommonJsImport(id); + if (requireCall === null) { + return null; + } + return {from: requireCall.arguments[0].text, name: id.text}; + } + + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + return this.getCommonJsImportedDeclaration(id) || super.getDeclarationOfIdentifier(id); + } + + getExportsOfModule(module: ts.Node): Map|null { + return super.getExportsOfModule(module) || this.getCommonJsExports(module.getSourceFile()); + } + + getCommonJsExports(sourceFile: ts.SourceFile): Map|null { + if (!this.commonJsExports.has(sourceFile)) { + const moduleExports = this.computeExportsOfCommonJsModule(sourceFile); + this.commonJsExports.set(sourceFile, moduleExports); + } + return this.commonJsExports.get(sourceFile) !; + } + + private computeExportsOfCommonJsModule(sourceFile: ts.SourceFile): Map { + const moduleMap = new Map(); + for (const statement of this.getModuleStatements(sourceFile)) { + if (isCommonJsExportStatement(statement)) { + const exportDeclaration = this.extractCommonJsExportDeclaration(statement); + if (exportDeclaration !== null) { + moduleMap.set(exportDeclaration.name, exportDeclaration.declaration); + } + } else if (isReexportStatement(statement)) { + const reexports = this.extractCommonJsReexports(statement, sourceFile); + for (const reexport of reexports) { + moduleMap.set(reexport.name, reexport.declaration); + } + } + } + return moduleMap; + } + + private extractCommonJsExportDeclaration(statement: CommonJsExportStatement): + CommonJsExportDeclaration|null { + const exportExpression = statement.expression.right; + const declaration = this.getDeclarationOfExpression(exportExpression); + if (declaration === null) { + return null; + } + const name = statement.expression.left.name.text; + return {name, declaration}; + } + + private extractCommonJsReexports(statement: ReexportStatement, containingFile: ts.SourceFile): + CommonJsExportDeclaration[] { + const reexports: CommonJsExportDeclaration[] = []; + const requireCall = statement.expression.arguments[0]; + const importPath = requireCall.arguments[0].text; + const importedFile = this.resolveModuleName(importPath, containingFile); + if (importedFile !== undefined) { + const viaModule = stripExtension(importedFile.fileName); + const importedExports = this.getExportsOfModule(importedFile); + if (importedExports !== null) { + importedExports.forEach( + (decl, name) => reexports.push({name, declaration: {node: decl.node, viaModule}})); + } + } + return reexports; + } + + private findCommonJsImport(id: ts.Identifier): RequireCall|null { + // Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`? + // If so capture the symbol of the namespace, e.g. `core`. + const nsIdentifier = findNamespaceOfIdentifier(id); + const nsSymbol = nsIdentifier && this.checker.getSymbolAtLocation(nsIdentifier) || null; + const nsDeclaration = nsSymbol && nsSymbol.valueDeclaration; + const initializer = + nsDeclaration && ts.isVariableDeclaration(nsDeclaration) && nsDeclaration.initializer || + null; + return initializer && isRequireCall(initializer) ? initializer : null; + } + + private getCommonJsImportedDeclaration(id: ts.Identifier): Declaration|null { + const importInfo = this.getImportOfIdentifier(id); + if (importInfo === null) { + return null; + } + + const importedFile = this.resolveModuleName(importInfo.from, id.getSourceFile()); + if (importedFile === undefined) { + return null; + } + + return {node: importedFile, viaModule: importInfo.from}; + } + + private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile + |undefined { + if (this.compilerHost.resolveModuleNames) { + const moduleInfo = + this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; + return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); + } else { + const moduleInfo = ts.resolveModuleName( + moduleName, containingFile.fileName, this.program.getCompilerOptions(), + this.compilerHost); + return moduleInfo.resolvedModule && + this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); + } + } +} + +type CommonJsExportStatement = ts.ExpressionStatement & { + expression: + ts.BinaryExpression & {left: ts.PropertyAccessExpression & {expression: ts.Identifier}} +}; +export function isCommonJsExportStatement(s: ts.Statement): s is CommonJsExportStatement { + return ts.isExpressionStatement(s) && ts.isBinaryExpression(s.expression) && + ts.isPropertyAccessExpression(s.expression.left) && + ts.isIdentifier(s.expression.left.expression) && + s.expression.left.expression.text === 'exports'; +} + +interface CommonJsExportDeclaration { + name: string; + declaration: Declaration; +} + +export type RequireCall = ts.CallExpression & {arguments: [ts.StringLiteral]}; +export function isRequireCall(node: ts.Node): node is RequireCall { + return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && + node.expression.text === 'require' && node.arguments.length === 1 && + ts.isStringLiteral(node.arguments[0]); +} + +/** + * If the identifier `id` is the RHS of a property access of the form `namespace.id` + * and `namespace` is an identifer then return `namespace`, otherwise `null`. + * @param id The identifier whose namespace we want to find. + */ +function findNamespaceOfIdentifier(id: ts.Identifier): ts.Identifier|null { + return id.parent && ts.isPropertyAccessExpression(id.parent) && + ts.isIdentifier(id.parent.expression) ? + id.parent.expression : + null; +} + +export function stripParentheses(node: ts.Node): ts.Node { + return ts.isParenthesizedExpression(node) ? node.expression : node; +} + +type ReexportStatement = ts.ExpressionStatement & {expression: {arguments: [RequireCall]}}; +function isReexportStatement(statement: ts.Statement): statement is ReexportStatement { + return ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && + ts.isIdentifier(statement.expression.expression) && + statement.expression.expression.text === '__export' && + statement.expression.arguments.length === 1 && + isRequireCall(statement.expression.arguments[0]); +} + +function stripExtension(fileName: string): string { + return fileName.replace(/\..+$/, ''); +} diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 3758af6851..b30074c49e 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -305,19 +305,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N return null; } - /** - * Determine if an identifier was imported from another module and return `Import` metadata - * describing its origin. - * - * @param id a TypeScript `ts.Identifer` to reflect. - * - * @returns metadata about the `Import` if the identifier was imported from another module, or - * `null` if the identifier doesn't resolve to an import but instead is locally defined. - */ - getImportOfIdentifier(id: ts.Identifier): Import|null { - return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); - } - /** * Find all the classes that contain decorations in a given file. * @param sourceFile The source file to search for decorated classes. @@ -325,7 +312,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] { const classes: DecoratedClass[] = []; - sourceFile.statements.map(statement => { + this.getModuleStatements(sourceFile).forEach(statement => { if (ts.isVariableStatement(statement)) { statement.declarationList.declarations.forEach(declaration => { const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration)); @@ -379,7 +366,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N throw new Error( `Cannot get the dts file for a declaration that has no name: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`); } - return this.dtsDeclarationMap.get(declaration.name.text) || null; + return this.dtsDeclarationMap.has(declaration.name.text) ? + this.dtsDeclarationMap.get(declaration.name.text) ! : + null; } /** @@ -432,7 +421,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected resolveAliasedClassIdentifier(declaration: ts.Declaration): ts.Identifier|null { this.ensurePreprocessed(declaration.getSourceFile()); - return this.aliasedClassDeclarations.get(declaration) || null; + return this.aliasedClassDeclarations.has(declaration) ? + this.aliasedClassDeclarations.get(declaration) ! : + null; } /** @@ -487,6 +478,16 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name); } + /** Get the top level statements for a module. + * + * In ES5 and ES2015 this is just the top level statements of the file. + * @param sourceFile The module whose statements we want. + * @returns An array of top level statements for the given module. + */ + protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] { + return Array.from(sourceFile.statements); + } + protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null { const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); if (decoratorsProperty) { @@ -741,7 +742,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N helperCall, makeMemberTargetFilter(classSymbol.name)); memberDecorators.forEach((decorators, memberName) => { if (memberName) { - const memberDecorators = memberDecoratorMap.get(memberName) || []; + const memberDecorators = + memberDecoratorMap.has(memberName) ? memberDecoratorMap.get(memberName) ! : []; const coreDecorators = decorators.filter(decorator => this.isFromCore(decorator)); memberDecoratorMap.set(memberName, memberDecorators.concat(coreDecorators)); } @@ -778,7 +780,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N if (keyName === undefined) { classDecorators.push(decorator); } else { - const decorators = memberDecorators.get(keyName) || []; + const decorators = + memberDecorators.has(keyName) ? memberDecorators.get(keyName) ! : []; decorators.push(decorator); memberDecorators.set(keyName, decorators); } @@ -877,14 +880,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const decorator = reflectObjectLiteral(node); // Is the value of the `type` property an identifier? - const typeIdentifier = decorator.get('type'); - if (typeIdentifier && ts.isIdentifier(typeIdentifier)) { - decorators.push({ - name: typeIdentifier.text, - identifier: typeIdentifier, - import: this.getImportOfIdentifier(typeIdentifier), node, - args: getDecoratorArgs(node), - }); + if (decorator.has('type')) { + let typeIdentifier = decorator.get('type') !; + if (ts.isPropertyAccessExpression(typeIdentifier)) { + // the type is in a namespace, e.g. `core.Directive` + typeIdentifier = typeIdentifier.name; + } + if (ts.isIdentifier(typeIdentifier)) { + decorators.push({ + name: typeIdentifier.text, + identifier: typeIdentifier, + import: this.getImportOfIdentifier(typeIdentifier), node, + args: getDecoratorArgs(node), + }); + } } } }); @@ -1033,8 +1042,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N */ protected getConstructorParameterDeclarations(classSymbol: ClassSymbol): ts.ParameterDeclaration[]|null { - const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR); - if (constructorSymbol) { + if (classSymbol.members && classSymbol.members.has(CONSTRUCTOR)) { + const constructorSymbol = classSymbol.members.get(CONSTRUCTOR) !; // For some reason the constructor does not have a `valueDeclaration` ?!? const constructor = constructorSymbol.declarations && constructorSymbol.declarations[0] as ts.ConstructorDeclaration | undefined; @@ -1110,8 +1119,10 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N element => ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null) .map(paramInfo => { - const typeExpression = paramInfo && paramInfo.get('type') || null; - const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const typeExpression = + paramInfo && paramInfo.has('type') ? paramInfo.get('type') ! : null; + const decoratorInfo = + paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators') ! : null; const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo) .filter(decorator => this.isFromCore(decorator)); @@ -1327,27 +1338,56 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N prop => !!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') || null; - const ngModuleIdentifier = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) && - ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer || - null; - // If no `ngModule` property was found in an object literal return value, return `null` to - // indicate that the provided node does not appear to be a `ModuleWithProviders` function. - if (ngModuleIdentifier === null) { + if (!ngModuleProperty || !ts.isPropertyAssignment(ngModuleProperty)) { return null; } - const ngModuleDeclaration = this.getDeclarationOfIdentifier(ngModuleIdentifier); + // The ngModuleValue could be of the form `SomeModule` or `namespace_1.SomeModule` + const ngModuleValue = ngModuleProperty.initializer; + if (!ts.isIdentifier(ngModuleValue) && !ts.isPropertyAccessExpression(ngModuleValue)) { + return null; + } + + const ngModuleDeclaration = this.getDeclarationOfExpression(ngModuleValue); if (!ngModuleDeclaration) { throw new Error( - `Cannot find a declaration for NgModule ${ngModuleIdentifier.text} referenced in "${declaration!.getText()}"`); + `Cannot find a declaration for NgModule ${ngModuleValue.getText()} referenced in "${declaration!.getText()}"`); } if (!hasNameIdentifier(ngModuleDeclaration.node)) { return null; } - const ngModule = ngModuleDeclaration as Declaration; + return { + name, + ngModule: ngModuleDeclaration as Declaration, declaration, container + }; + } - return {name, ngModule, declaration, container}; + protected getDeclarationOfExpression(expression: ts.Expression): Declaration|null { + if (ts.isIdentifier(expression)) { + return this.getDeclarationOfIdentifier(expression); + } + + if (!ts.isPropertyAccessExpression(expression) || !ts.isIdentifier(expression.expression)) { + return null; + } + + const namespaceDecl = this.getDeclarationOfIdentifier(expression.expression); + if (!namespaceDecl || !ts.isSourceFile(namespaceDecl.node)) { + return null; + } + + const namespaceExports = this.getExportsOfModule(namespaceDecl.node); + if (namespaceExports === null) { + return null; + } + + if (!namespaceExports.has(expression.name.text)) { + return null; + } + + const exportDecl = namespaceExports.get(expression.name.text) !; + return {...exportDecl, viaModule: namespaceDecl.viaModule}; } } diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index a2354a40db..6fcfb09baa 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -339,8 +339,9 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { if (expression && ts.isArrayLiteralExpression(expression)) { const elements = expression.elements; return elements.map(reflectArrayElement).map(paramInfo => { - const typeExpression = paramInfo && paramInfo.get('type') || null; - const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const typeExpression = paramInfo && paramInfo.has('type') ? paramInfo.get('type') ! : null; + const decoratorInfo = + paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators') ! : null; const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo); return {typeExpression, decorators}; }); diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts new file mode 100644 index 0000000000..33283a6380 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -0,0 +1,278 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {Declaration, Import} from '../../../src/ngtsc/reflection'; +import {Logger} from '../logging/logger'; +import {BundleProgram} from '../packages/bundle_program'; +import {Esm5ReflectionHost} from './esm5_host'; + +export class UmdReflectionHost extends Esm5ReflectionHost { + protected umdModules = new Map(); + protected umdExports = new Map|null>(); + protected umdImportPaths = new Map(); + constructor( + logger: Logger, isCore: boolean, protected program: ts.Program, + protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) { + super(logger, isCore, program.getTypeChecker(), dts); + } + + getImportOfIdentifier(id: ts.Identifier): Import|null { + const importParameter = this.findUmdImportParameter(id); + const from = importParameter && this.getUmdImportPath(importParameter); + return from !== null ? {from, name: id.text} : null; + } + + getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + return this.getUmdImportedDeclaration(id) || super.getDeclarationOfIdentifier(id); + } + + getExportsOfModule(module: ts.Node): Map|null { + return super.getExportsOfModule(module) || this.getUmdExports(module.getSourceFile()); + } + + getUmdModule(sourceFile: ts.SourceFile): UmdModule|null { + if (sourceFile.isDeclarationFile) { + return null; + } + if (!this.umdModules.has(sourceFile)) { + if (sourceFile.statements.length !== 1) { + throw new Error( + `Expected UMD module file (${sourceFile.fileName}) to contain exactly one statement, but found ${sourceFile.statements}.`); + } + this.umdModules.set(sourceFile, parseStatementForUmdModule(sourceFile.statements[0])); + } + return this.umdModules.get(sourceFile) !; + } + + getUmdImportPath(importParameter: ts.ParameterDeclaration): string|null { + if (this.umdImportPaths.has(importParameter)) { + return this.umdImportPaths.get(importParameter) !; + } + + const umdModule = this.getUmdModule(importParameter.getSourceFile()); + if (umdModule === null) { + return null; + } + + const imports = getImportsOfUmdModule(umdModule); + if (imports === null) { + return null; + } + + for (const i of imports) { + this.umdImportPaths.set(i.parameter, i.path); + if (i.parameter === importParameter) { + return i.path; + } + } + + return null; + } + + getUmdExports(sourceFile: ts.SourceFile): Map|null { + if (!this.umdExports.has(sourceFile)) { + const moduleExports = this.computeExportsOfUmdModule(sourceFile); + this.umdExports.set(sourceFile, moduleExports); + } + return this.umdExports.get(sourceFile) !; + } + + /** Get the top level statements for a module. + * + * In UMD modules these are the body of the UMD factory function. + * + * @param sourceFile The module whose statements we want. + * @returns An array of top level statements for the given module. + */ + protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] { + const umdModule = this.getUmdModule(sourceFile); + return umdModule !== null ? Array.from(umdModule.factoryFn.body.statements) : []; + } + + private computeExportsOfUmdModule(sourceFile: ts.SourceFile): Map|null { + const moduleMap = new Map(); + const exportStatements = this.getModuleStatements(sourceFile).filter(isUmdExportStatement); + const exportDeclarations = + exportStatements.map(statement => this.extractUmdExportDeclaration(statement)); + exportDeclarations.forEach(decl => { + if (decl) { + moduleMap.set(decl.name, decl.declaration); + } + }); + return moduleMap; + } + + private extractUmdExportDeclaration(statement: UmdExportStatement): UmdExportDeclaration|null { + const exportExpression = statement.expression.right; + const name = statement.expression.left.name.text; + + const declaration = this.getDeclarationOfExpression(exportExpression); + if (declaration === null) { + return null; + } + + return {name, declaration}; + } + + private findUmdImportParameter(id: ts.Identifier): ts.ParameterDeclaration|null { + // Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`? + // If so capture the symbol of the namespace, e.g. `core`. + const nsIdentifier = findNamespaceOfIdentifier(id); + const nsSymbol = nsIdentifier && this.checker.getSymbolAtLocation(nsIdentifier) || null; + + // Is the namespace a parameter on a UMD factory function, e.g. `function factory(this, core)`? + // If so then return its declaration. + const nsDeclaration = nsSymbol && nsSymbol.valueDeclaration; + return nsDeclaration && ts.isParameter(nsDeclaration) ? nsDeclaration : null; + } + + private getUmdImportedDeclaration(id: ts.Identifier): Declaration|null { + const importInfo = this.getImportOfIdentifier(id); + if (importInfo === null) { + return null; + } + + const importedFile = this.resolveModuleName(importInfo.from, id.getSourceFile()); + if (importedFile === undefined) { + return null; + } + + // We need to add the `viaModule` because the `getExportsOfModule()` call + // did not know that we were importing the declaration. + return {node: importedFile, viaModule: importInfo.from}; + } + + private resolveModuleName(moduleName: string, containingFile: ts.SourceFile): ts.SourceFile + |undefined { + if (this.compilerHost.resolveModuleNames) { + const moduleInfo = + this.compilerHost.resolveModuleNames([moduleName], containingFile.fileName)[0]; + return moduleInfo && this.program.getSourceFile(moduleInfo.resolvedFileName); + } else { + const moduleInfo = ts.resolveModuleName( + moduleName, containingFile.fileName, this.program.getCompilerOptions(), + this.compilerHost); + return moduleInfo.resolvedModule && + this.program.getSourceFile(moduleInfo.resolvedModule.resolvedFileName); + } + } +} + +export function parseStatementForUmdModule(statement: ts.Statement): UmdModule|null { + const wrapperCall = getUmdWrapperCall(statement); + if (!wrapperCall) return null; + + const wrapperFn = wrapperCall.expression; + if (!ts.isFunctionExpression(wrapperFn)) return null; + + const factoryFnParamIndex = wrapperFn.parameters.findIndex( + parameter => ts.isIdentifier(parameter.name) && parameter.name.text === 'factory'); + if (factoryFnParamIndex === -1) return null; + + const factoryFn = stripParentheses(wrapperCall.arguments[factoryFnParamIndex]); + if (!factoryFn || !ts.isFunctionExpression(factoryFn)) return null; + + return {wrapperFn, factoryFn}; +} + +function getUmdWrapperCall(statement: ts.Statement): ts.CallExpression& + {expression: ts.FunctionExpression}|null { + if (!ts.isExpressionStatement(statement) || !ts.isParenthesizedExpression(statement.expression) || + !ts.isCallExpression(statement.expression.expression) || + !ts.isFunctionExpression(statement.expression.expression.expression)) { + return null; + } + return statement.expression.expression as ts.CallExpression & {expression: ts.FunctionExpression}; +} + + +export function getImportsOfUmdModule(umdModule: UmdModule): + {parameter: ts.ParameterDeclaration, path: string}[] { + const imports: {parameter: ts.ParameterDeclaration, path: string}[] = []; + for (let i = 1; i < umdModule.factoryFn.parameters.length; i++) { + imports.push({ + parameter: umdModule.factoryFn.parameters[i], + path: getRequiredModulePath(umdModule.wrapperFn, i) + }); + } + return imports; +} + +interface UmdModule { + wrapperFn: ts.FunctionExpression; + factoryFn: ts.FunctionExpression; +} + +type UmdExportStatement = ts.ExpressionStatement & { + expression: + ts.BinaryExpression & {left: ts.PropertyAccessExpression & {expression: ts.Identifier}} +}; + +function isUmdExportStatement(s: ts.Statement): s is UmdExportStatement { + return ts.isExpressionStatement(s) && ts.isBinaryExpression(s.expression) && + ts.isPropertyAccessExpression(s.expression.left) && + ts.isIdentifier(s.expression.left.expression) && + s.expression.left.expression.text === 'exports'; +} + +interface UmdExportDeclaration { + name: string; + declaration: Declaration; +} + +function getRequiredModulePath(wrapperFn: ts.FunctionExpression, paramIndex: number): string { + const statement = wrapperFn.body.statements[0]; + if (!ts.isExpressionStatement(statement)) { + throw new Error( + 'UMD wrapper body is not an expression statement:\n' + wrapperFn.body.getText()); + } + const modulePaths: string[] = []; + findModulePaths(statement.expression); + + // Since we were only interested in the `require()` calls, we miss the `exports` argument, so we + // need to subtract 1. + // E.g. `function(exports, dep1, dep2)` maps to `function(exports, require('path/to/dep1'), + // require('path/to/dep2'))` + return modulePaths[paramIndex - 1]; + + // Search the statement for calls to `require('...')` and extract the string value of the first + // argument + function findModulePaths(node: ts.Node) { + if (isRequireCall(node)) { + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + modulePaths.push(argument.text); + } + } else { + node.forEachChild(findModulePaths); + } + } +} + +function isRequireCall(node: ts.Node): node is ts.CallExpression { + return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && + node.expression.text === 'require' && node.arguments.length === 1; +} + +/** + * If the identifier `id` is the RHS of a property access of the form `namespace.id` + * and `namespace` is an identifer then return `namespace`, otherwise `null`. + * @param id The identifier whose namespace we want to find. + */ +function findNamespaceOfIdentifier(id: ts.Identifier): ts.Identifier|null { + return id.parent && ts.isPropertyAccessExpression(id.parent) && + ts.isIdentifier(id.parent.expression) ? + id.parent.expression : + null; +} + +export function stripParentheses(node: ts.Node): ts.Node { + return ts.isParenthesizedExpression(node) ? node.expression : node; +} \ No newline at end of file diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index 4a760b3e63..ec03384e23 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -7,9 +7,11 @@ */ import {AbsoluteFsPath} from '../../src/ngtsc/path'; +import {CommonJsDependencyHost} from './dependencies/commonjs_dependency_host'; import {DependencyResolver} from './dependencies/dependency_resolver'; import {EsmDependencyHost} from './dependencies/esm_dependency_host'; import {ModuleResolver} from './dependencies/module_resolver'; +import {UmdDependencyHost} from './dependencies/umd_dependency_host'; import {FileSystem} from './file_system/file_system'; import {NodeJSFileSystem} from './file_system/node_js_file_system'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; @@ -63,7 +65,7 @@ export interface NgccOptions { pathMappings?: PathMappings; } -const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015']; +const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015', 'umd', 'commonjs']; /** * This is the main entry-point into ngcc (aNGular Compatibility Compiler). @@ -80,8 +82,15 @@ export function mainNgcc( const fs = new NodeJSFileSystem(); const transformer = new Transformer(fs, logger); const moduleResolver = new ModuleResolver(fs, pathMappings); - const host = new EsmDependencyHost(fs, moduleResolver); - const resolver = new DependencyResolver(logger, host); + const esmDependencyHost = new EsmDependencyHost(fs, moduleResolver); + const umdDependencyHost = new UmdDependencyHost(fs, moduleResolver); + const commonJsDependencyHost = new CommonJsDependencyHost(fs, moduleResolver); + const resolver = new DependencyResolver(fs, logger, { + esm5: esmDependencyHost, + esm2015: esmDependencyHost, + umd: umdDependencyHost, + commonjs: commonJsDependencyHost + }); const finder = new EntryPointFinder(fs, logger, resolver); const fileWriter = getFileWriter(fs, createNewEntryPointFormats); @@ -124,7 +133,7 @@ export function mainNgcc( for (let i = 0; i < propertiesToConsider.length; i++) { const property = propertiesToConsider[i] as EntryPointJsonProperty; const formatPath = entryPointPackageJson[property]; - const format = getEntryPointFormat(property); + const format = getEntryPointFormat(fs, entryPoint, property); // No format then this property is not supposed to be compiled. if (!formatPath || !format || SUPPORTED_FORMATS.indexOf(format) === -1) continue; diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index 8a8c8bd9bc..d0e485fb8d 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -5,14 +5,16 @@ * 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 * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; +import {parseStatementForUmdModule} from '../host/umd_host'; import {Logger} from '../logging/logger'; /** * The possible values for the format of an entry-point. */ -export type EntryPointFormat = 'esm5' | 'esm2015' | 'umd'; +export type EntryPointFormat = 'esm5' | 'esm2015' | 'umd' | 'commonjs'; /** * An object containing information about an entry-point, including paths @@ -107,7 +109,8 @@ export function getEntryPointInfo( * @param property The property to convert to a format. * @returns An entry-point format or `undefined` if none match the given property. */ -export function getEntryPointFormat(property: string): EntryPointFormat|undefined { +export function getEntryPointFormat( + fs: FileSystem, entryPoint: EntryPoint, property: string): EntryPointFormat|undefined { switch (property) { case 'fesm2015': return 'esm2015'; @@ -120,7 +123,8 @@ export function getEntryPointFormat(property: string): EntryPointFormat|undefine case 'esm5': return 'esm5'; case 'main': - return 'umd'; + const pathToMain = AbsoluteFsPath.join(entryPoint.path, entryPoint.packageJson['main'] !); + return isUmdModule(fs, pathToMain) ? 'umd' : 'commonjs'; case 'module': return 'esm5'; default: @@ -143,3 +147,10 @@ function loadEntryPointPackage( return null; } } + +function isUmdModule(fs: FileSystem, sourceFilePath: AbsoluteFsPath): boolean { + const sourceFile = + ts.createSourceFile(sourceFilePath, fs.readFile(sourceFilePath), ts.ScriptTarget.ES5); + return sourceFile.statements.length > 0 && + parseStatementForUmdModule(sourceFile.statements[0]) !== null; +} diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts index f739669480..d1546a7466 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts @@ -92,7 +92,7 @@ export class EntryPointFinder { entryPoints.push(...this.getEntryPointsForPackage(packagePath)); // Also check for any nested node_modules in this package - const nestedNodeModulesPath = AbsoluteFsPath.resolve(packagePath, 'node_modules'); + const nestedNodeModulesPath = AbsoluteFsPath.join(packagePath, 'node_modules'); if (this.fs.exists(nestedNodeModulesPath)) { entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath)); } diff --git a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts index 19ddb626dd..d437a51945 100644 --- a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -27,7 +27,7 @@ export class NgccCompilerHost implements ts.CompilerHost { } getDefaultLibLocation(): string { - const nodeLibPath = AbsoluteFsPath.fromUnchecked(require.resolve('typescript')); + const nodeLibPath = AbsoluteFsPath.from(require.resolve('typescript')); return AbsoluteFsPath.join(nodeLibPath, '..'); } diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index 3abc7078a3..25a972f223 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -13,13 +13,20 @@ import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {FileSystem} from '../file_system/file_system'; +import {CommonJsReflectionHost} from '../host/commonjs_host'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; +import {UmdReflectionHost} from '../host/umd_host'; import {Logger} from '../logging/logger'; -import {Esm5Renderer} from '../rendering/esm5_renderer'; -import {EsmRenderer} from '../rendering/esm_renderer'; -import {FileInfo, Renderer} from '../rendering/renderer'; +import {CommonJsRenderingFormatter} from '../rendering/commonjs_rendering_formatter'; +import {DtsRenderer} from '../rendering/dts_renderer'; +import {Esm5RenderingFormatter} from '../rendering/esm5_rendering_formatter'; +import {EsmRenderingFormatter} from '../rendering/esm_rendering_formatter'; +import {Renderer} from '../rendering/renderer'; +import {RenderingFormatter} from '../rendering/rendering_formatter'; +import {UmdRenderingFormatter} from '../rendering/umd_rendering_formatter'; +import {FileToWrite} from '../rendering/utils'; import {EntryPointBundle} from './entry_point_bundle'; @@ -54,7 +61,7 @@ export class Transformer { * @param bundle the bundle to transform. * @returns information about the files that were transformed. */ - transform(bundle: EntryPointBundle): FileInfo[] { + transform(bundle: EntryPointBundle): FileToWrite[] { const isCore = bundle.isCore; const reflectionHost = this.getHost(isCore, bundle); @@ -63,10 +70,21 @@ export class Transformer { moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle); // Transform the source files and source maps. - const renderer = this.getRenderer(reflectionHost, isCore, bundle); - const renderedFiles = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + const srcFormatter = this.getRenderingFormatter(reflectionHost, isCore, bundle); + + const renderer = + new Renderer(srcFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + let renderedFiles = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + + if (bundle.dts) { + const dtsFormatter = new EsmRenderingFormatter(reflectionHost, isCore); + const dtsRenderer = + new DtsRenderer(dtsFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + const renderedDtsFiles = dtsRenderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + renderedFiles = renderedFiles.concat(renderedDtsFiles); + } return renderedFiles; } @@ -78,17 +96,31 @@ export class Transformer { return new Esm2015ReflectionHost(this.logger, isCore, typeChecker, bundle.dts); case 'esm5': return new Esm5ReflectionHost(this.logger, isCore, typeChecker, bundle.dts); + case 'umd': + return new UmdReflectionHost( + this.logger, isCore, bundle.src.program, bundle.src.host, bundle.dts); + case 'commonjs': + return new CommonJsReflectionHost( + this.logger, isCore, bundle.src.program, bundle.src.host, bundle.dts); default: throw new Error(`Reflection host for "${bundle.format}" not yet implemented.`); } } - getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer { + getRenderingFormatter(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): + RenderingFormatter { switch (bundle.format) { case 'esm2015': - return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); + return new EsmRenderingFormatter(host, isCore); case 'esm5': - return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); + return new Esm5RenderingFormatter(host, isCore); + case 'umd': + if (!(host instanceof UmdReflectionHost)) { + throw new Error('UmdRenderer requires a UmdReflectionHost'); + } + return new UmdRenderingFormatter(host, isCore); + case 'commonjs': + return new CommonJsRenderingFormatter(host, isCore); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } diff --git a/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts new file mode 100644 index 0000000000..5b3010bdd3 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/commonjs_rendering_formatter.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google Inc. 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 {dirname, relative} from 'canonical-path'; +import * as ts from 'typescript'; +import MagicString from 'magic-string'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {isRequireCall} from '../host/commonjs_host'; +import {NgccReflectionHost} from '../host/ngcc_host'; +import {Esm5RenderingFormatter} from './esm5_rendering_formatter'; +import {stripExtension} from './utils'; + +/** + * A RenderingFormatter that works with CommonJS files, instead of `import` and `export` statements + * the module is an IIFE with a factory function call with dependencies, which are defined in a + * wrapper function for AMD, CommonJS and global module formats. + */ +export class CommonJsRenderingFormatter extends Esm5RenderingFormatter { + constructor(protected commonJsHost: NgccReflectionHost, isCore: boolean) { + super(commonJsHost, isCore); + } + + /** + * Add the imports below any in situ imports as `require` calls. + */ + addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { + const insertionPoint = this.findEndOfImports(file); + const renderedImports = + imports.map(i => `var ${i.qualifier} = require('${i.specifier}');\n`).join(''); + output.appendLeft(insertionPoint, renderedImports); + } + + /** + * Add the exports to the bottom of the file. + */ + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + exports.forEach(e => { + const basePath = stripExtension(e.from); + const relativePath = './' + relative(dirname(entryPointBasePath), basePath); + const namedImport = entryPointBasePath !== basePath ? + importManager.generateNamedImport(relativePath, e.identifier) : + {symbol: e.identifier, moduleImport: null}; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; + output.append(exportStr); + }); + } + + protected findEndOfImports(sf: ts.SourceFile): number { + for (const statement of sf.statements) { + if (ts.isExpressionStatement(statement) && isRequireCall(statement.expression)) { + continue; + } + const declarations = ts.isVariableStatement(statement) ? + Array.from(statement.declarationList.declarations) : + []; + if (declarations.some(d => !d.initializer || !isRequireCall(d.initializer))) { + return statement.getStart(); + } + } + return 0; + } +} diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts new file mode 100644 index 0000000000..6329506136 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts @@ -0,0 +1,161 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; + +import {translateType, ImportManager} from '../../../src/ngtsc/translator'; +import {DecorationAnalyses} from '../analysis/decoration_analyzer'; +import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {FileSystem} from '../file_system/file_system'; +import {NgccReflectionHost} from '../host/ngcc_host'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {Logger} from '../logging/logger'; +import {FileToWrite, getImportRewriter} from './utils'; +import {RenderingFormatter} from './rendering_formatter'; +import {extractSourceMap, renderSourceAndMap} from './source_maps'; +import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform'; + +/** + * A structure that captures information about what needs to be rendered + * in a typings file. + * + * It is created as a result of processing the analysis passed to the renderer. + * + * The `renderDtsFile()` method consumes it when rendering a typings file. + */ +class DtsRenderInfo { + classInfo: DtsClassInfo[] = []; + moduleWithProviders: ModuleWithProvidersInfo[] = []; + privateExports: ExportInfo[] = []; +} + + +/** + * Information about a class in a typings file. + */ +export interface DtsClassInfo { + dtsDeclaration: ts.Declaration; + compilation: CompileResult[]; +} + +/** + * A base-class for rendering an `AnalyzedFile`. + * + * Package formats have output files that must be rendered differently. Concrete sub-classes must + * implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods. + */ +export class DtsRenderer { + constructor( + private dtsFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger, + private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) { + } + + renderProgram( + decorationAnalyses: DecorationAnalyses, + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileToWrite[] { + const renderedFiles: FileToWrite[] = []; + + // Transform the .d.ts files + if (this.bundle.dts) { + const dtsFiles = this.getTypingsFilesToRender( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + // If the dts entry-point is not already there (it did not have compiled classes) + // then add it now, to ensure it gets its extra exports rendered. + if (!dtsFiles.has(this.bundle.dts.file)) { + dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo()); + } + dtsFiles.forEach( + (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo))); + } + + return renderedFiles; + } + + renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileToWrite[] { + const input = extractSourceMap(this.fs, this.logger, dtsFile); + const outputText = new MagicString(input.source); + const printer = ts.createPrinter(); + const importManager = new ImportManager( + getImportRewriter(this.bundle.dts !.r3SymbolsFile, this.isCore, false), IMPORT_PREFIX); + + renderInfo.classInfo.forEach(dtsClass => { + const endOfClass = dtsClass.dtsDeclaration.getEnd(); + dtsClass.compilation.forEach(declaration => { + const type = translateType(declaration.type, importManager); + const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile); + const newStatement = ` static ${declaration.name}: ${typeStr};\n`; + outputText.appendRight(endOfClass - 1, newStatement); + }); + }); + + this.dtsFormatter.addModuleWithProvidersParams( + outputText, renderInfo.moduleWithProviders, importManager); + this.dtsFormatter.addExports( + outputText, dtsFile.fileName, renderInfo.privateExports, importManager, dtsFile); + this.dtsFormatter.addImports( + outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); + + + + return renderSourceAndMap(dtsFile, input, outputText); + } + + private getTypingsFilesToRender( + decorationAnalyses: DecorationAnalyses, + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses| + null): Map { + const dtsMap = new Map(); + + // Capture the rendering info from the decoration analyses + decorationAnalyses.forEach(compiledFile => { + compiledFile.compiledClasses.forEach(compiledClass => { + const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); + if (dtsDeclaration) { + const dtsFile = dtsDeclaration.getSourceFile(); + const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo(); + renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation}); + dtsMap.set(dtsFile, renderInfo); + } + }); + }); + + // Capture the ModuleWithProviders functions/methods that need updating + if (moduleWithProvidersAnalyses !== null) { + moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => { + const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo(); + renderInfo.moduleWithProviders = moduleWithProvidersToFix; + dtsMap.set(dtsFile, renderInfo); + }); + } + + // Capture the private declarations that need to be re-exported + if (privateDeclarationsAnalyses.length) { + privateDeclarationsAnalyses.forEach(e => { + if (!e.dtsFrom && !e.alias) { + throw new Error( + `There is no typings path for ${e.identifier} in ${e.from}.\n` + + `We need to add an export for this class to a .d.ts typings file because ` + + `Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` + + `The simplest fix for this is to ensure that this class is exported from the package's entry-point.`); + } + }); + const dtsEntryPoint = this.bundle.dts !.file; + const renderInfo = + dtsMap.has(dtsEntryPoint) ? dtsMap.get(dtsEntryPoint) ! : new DtsRenderInfo(); + renderInfo.privateExports = privateDeclarationsAnalyses; + dtsMap.set(dtsEntryPoint, renderInfo); + } + + return dtsMap; + } +} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts similarity index 68% rename from packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts rename to packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts index 368c93b44f..8b22f7519f 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts @@ -8,22 +8,16 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {CompiledClass} from '../analysis/decoration_analyzer'; -import {FileSystem} from '../file_system/file_system'; import {getIifeBody} from '../host/esm5_host'; -import {NgccReflectionHost} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {EsmRenderer} from './esm_renderer'; - -export class Esm5Renderer extends EsmRenderer { - constructor( - fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } +import {EsmRenderingFormatter} from './esm_rendering_formatter'; +/** + * A RenderingFormatter that works with files that use ECMAScript Module `import` and `export` + * statements, but instead of `class` declarations it uses ES5 `function` wrappers for classes. + */ +export class Esm5RenderingFormatter extends EsmRenderingFormatter { /** - * Add the definitions to each decorated class + * Add the definitions inside the IIFE of each decorated class */ addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { const iifeBody = getIifeBody(compiledClass.declaration); diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts deleted file mode 100644 index 15fe6f61fb..0000000000 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 MagicString from 'magic-string'; -import * as ts from 'typescript'; -import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; -import {CompiledClass} from '../analysis/decoration_analyzer'; -import {ExportInfo} from '../analysis/private_declarations_analyzer'; -import {FileSystem} from '../file_system/file_system'; -import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; -import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer'; - -export class EsmRenderer extends Renderer { - constructor( - fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } - - /** - * Add the imports at the top of the file - */ - addImports( - output: MagicString, imports: {specifier: string; qualifier: string;}[], - sf: ts.SourceFile): void { - const insertionPoint = findEndOfImports(sf); - const renderedImports = - imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join(''); - output.appendLeft(insertionPoint, renderedImports); - } - - addExports(output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void { - exports.forEach(e => { - let exportFrom = ''; - const isDtsFile = isDtsPath(entryPointBasePath); - const from = isDtsFile ? e.dtsFrom : e.from; - - if (from) { - const basePath = stripExtension(from); - const relativePath = - './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); - exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; - } - - // aliases should only be added in dts files as these are lost when rolling up dts file. - const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier; - const exportStr = `\nexport {${exportStatement}}${exportFrom};`; - output.append(exportStr); - }); - } - - addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { - if (constants === '') { - return; - } - const insertionPoint = findEndOfImports(file); - - // Append the constants to the right of the insertion point, to ensure they get ordered after - // added imports (those are appended left to the insertion point). - output.appendRight(insertionPoint, '\n' + constants + '\n'); - } - - /** - * Add the definitions to each decorated class - */ - addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { - const classSymbol = this.host.getClassSymbol(compiledClass.declaration); - if (!classSymbol) { - throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); - } - const insertionPoint = classSymbol.valueDeclaration !.getEnd(); - output.appendLeft(insertionPoint, '\n' + definitions); - } - - /** - * Remove static decorator properties from classes - */ - removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void { - decoratorsToRemove.forEach((nodesToRemove, containerNode) => { - if (ts.isArrayLiteralExpression(containerNode)) { - const items = containerNode.elements; - if (items.length === nodesToRemove.length) { - // Remove the entire statement - const statement = findStatement(containerNode); - if (statement) { - output.remove(statement.getFullStart(), statement.getEnd()); - } - } else { - nodesToRemove.forEach(node => { - // remove any trailing comma - const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? - node.getEnd() + 1 : - node.getEnd(); - output.remove(node.getFullStart(), end); - }); - } - } - }); - } - - rewriteSwitchableDeclarations( - outputText: MagicString, sourceFile: ts.SourceFile, - declarations: SwitchableVariableDeclaration[]): void { - declarations.forEach(declaration => { - const start = declaration.initializer.getStart(); - const end = declaration.initializer.getEnd(); - const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER); - outputText.overwrite(start, end, replacement); - }); - } -} - -function findEndOfImports(sf: ts.SourceFile): number { - for (const stmt of sf.statements) { - if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) && - !ts.isNamespaceImport(stmt)) { - return stmt.getStart(); - } - } - - return 0; -} - -function findStatement(node: ts.Node) { - while (node) { - if (ts.isExpressionStatement(node)) { - return node; - } - node = node.parent; - } - return undefined; -} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts new file mode 100644 index 0000000000..579fde535b --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -0,0 +1,218 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter'; +import {stripExtension} from './utils'; + +/** + * A RenderingFormatter that works with ECMAScript Module import and export statements. + */ +export class EsmRenderingFormatter implements RenderingFormatter { + constructor(protected host: NgccReflectionHost, protected isCore: boolean) {} + + /** + * Add the imports at the top of the file, after any imports that are already there. + */ + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void { + const insertionPoint = this.findEndOfImports(sf); + const renderedImports = + imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join(''); + output.appendLeft(insertionPoint, renderedImports); + } + + /** + * Add the exports to the end of the file. + */ + addExports( + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + exports.forEach(e => { + let exportFrom = ''; + const isDtsFile = isDtsPath(entryPointBasePath); + const from = isDtsFile ? e.dtsFrom : e.from; + + if (from) { + const basePath = stripExtension(from); + const relativePath = + './' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath); + exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : ''; + } + + // aliases should only be added in dts files as these are lost when rolling up dts file. + const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier; + const exportStr = `\nexport {${exportStatement}}${exportFrom};`; + output.append(exportStr); + }); + } + + /** + * Add the constants directly after the imports. + */ + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const insertionPoint = this.findEndOfImports(file); + + // Append the constants to the right of the insertion point, to ensure they get ordered after + // added imports (those are appended left to the insertion point). + output.appendRight(insertionPoint, '\n' + constants + '\n'); + } + + /** + * Add the definitions directly after their decorated class. + */ + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { + const classSymbol = this.host.getClassSymbol(compiledClass.declaration); + if (!classSymbol) { + throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); + } + const insertionPoint = classSymbol.valueDeclaration !.getEnd(); + output.appendLeft(insertionPoint, '\n' + definitions); + } + + /** + * Remove static decorator properties from classes. + */ + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void { + decoratorsToRemove.forEach((nodesToRemove, containerNode) => { + if (ts.isArrayLiteralExpression(containerNode)) { + const items = containerNode.elements; + if (items.length === nodesToRemove.length) { + // Remove the entire statement + const statement = findStatement(containerNode); + if (statement) { + output.remove(statement.getFullStart(), statement.getEnd()); + } + } else { + nodesToRemove.forEach(node => { + // remove any trailing comma + const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? + node.getEnd() + 1 : + node.getEnd(); + output.remove(node.getFullStart(), end); + }); + } + } + }); + } + + /** + * Rewrite the the IVY switch markers to indicate we are in IVY mode. + */ + rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void { + declarations.forEach(declaration => { + const start = declaration.initializer.getStart(); + const end = declaration.initializer.getEnd(); + const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER); + outputText.overwrite(start, end, replacement); + }); + } + + + /** + * Add the type parameters to the appropriate functions that return `ModuleWithProviders` + * structures. + * + * This function will only get called on typings files. + */ + addModuleWithProvidersParams( + outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + moduleWithProviders.forEach(info => { + const ngModuleName = info.ngModule.node.name.text; + const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); + const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); + const importPath = info.ngModule.viaModule || + (declarationFile !== ngModuleFile ? + stripExtension( + `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : + null); + const ngModule = generateImportString(importManager, importPath, ngModuleName); + + if (info.declaration.type) { + const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ? + info.declaration.type.typeName : + null; + if (this.isCoreModuleWithProvidersType(typeName)) { + // The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type + // parameter adding. + outputText.overwrite( + info.declaration.type.getStart(), info.declaration.type.getEnd(), + `ModuleWithProviders<${ngModule}>`); + } else { + // The declaration returns an unknown type so we need to convert it to a union that + // includes the ngModule property. + const originalTypeString = info.declaration.type.getText(); + outputText.overwrite( + info.declaration.type.getStart(), info.declaration.type.getEnd(), + `(${originalTypeString})&{ngModule:${ngModule}}`); + } + } else { + // The declaration has no return type so provide one. + const lastToken = info.declaration.getLastToken(); + const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ? + lastToken.getStart() : + info.declaration.getEnd(); + outputText.appendLeft( + insertPoint, + `: ${generateImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`); + } + }); + } + + protected findEndOfImports(sf: ts.SourceFile): number { + for (const stmt of sf.statements) { + if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) && + !ts.isNamespaceImport(stmt)) { + return stmt.getStart(); + } + } + return 0; + } + + + + /** + * Check whether the given type is the core Angular `ModuleWithProviders` interface. + * @param typeName The type to check. + * @returns true if the type is the core Angular `ModuleWithProviders` interface. + */ + private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) { + const id = + typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null; + return ( + id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core')); + } +} + +function findStatement(node: ts.Node) { + while (node) { + if (ts.isExpressionStatement(node)) { + return node; + } + node = node.parent; + } + return undefined; +} + +function generateImportString( + importManager: ImportManager, importPath: string | null, importName: string) { + const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; + return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index 911a8aac75..136c401fee 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -6,73 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler'; -import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; import MagicString from 'magic-string'; -import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; import * as ts from 'typescript'; -import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; -import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; -import {CompileResult} from '../../../src/ngtsc/transform'; -import {translateStatement, translateType, ImportManager} from '../../../src/ngtsc/translator'; - +import {NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports'; +import {translateStatement, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer'; -import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer'; -import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer'; +import {PrivateDeclarationsAnalyses} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../constants'; import {FileSystem} from '../file_system/file_system'; -import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host'; -import {Logger} from '../logging/logger'; +import {NgccReflectionHost} from '../host/ngcc_host'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; - -interface SourceMapInfo { - source: string; - map: SourceMapConverter|null; - isInline: boolean; -} - -/** - * Information about a file that has been rendered. - */ -export interface FileInfo { - /** - * Path to where the file should be written. - */ - path: AbsoluteFsPath; - /** - * The contents of the file to be be written. - */ - contents: string; -} - -interface DtsClassInfo { - dtsDeclaration: ts.Declaration; - compilation: CompileResult[]; -} - -/** - * A structure that captures information about what needs to be rendered - * in a typings file. - * - * It is created as a result of processing the analysis passed to the renderer. - * - * The `renderDtsFile()` method consumes it when rendering a typings file. - */ -class DtsRenderInfo { - classInfo: DtsClassInfo[] = []; - moduleWithProviders: ModuleWithProvidersInfo[] = []; - privateExports: ExportInfo[] = []; -} - -/** - * The collected decorators that have become redundant after the compilation - * of Ivy static fields. The map is keyed by the container node, such that we - * can tell if we should remove the entire decorator property - */ -export type RedundantDecoratorMap = Map; -export const RedundantDecoratorMap = Map; +import {Logger} from '../logging/logger'; +import {FileToWrite, getImportRewriter, stripExtension} from './utils'; +import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter'; +import {extractSourceMap, renderSourceAndMap} from './source_maps'; /** * A base-class for rendering an `AnalyzedFile`. @@ -80,42 +29,28 @@ export const RedundantDecoratorMap = Map; * Package formats have output files that must be rendered differently. Concrete sub-classes must * implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods. */ -export abstract class Renderer { +export class Renderer { constructor( - protected fs: FileSystem, protected logger: Logger, protected host: NgccReflectionHost, - protected isCore: boolean, protected bundle: EntryPointBundle) {} + private srcFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger, + private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) { + } renderProgram( decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, - moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] { - const renderedFiles: FileInfo[] = []; + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] { + const renderedFiles: FileToWrite[] = []; // Transform the source files. this.bundle.src.program.getSourceFiles().forEach(sourceFile => { - const compiledFile = decorationAnalyses.get(sourceFile); - const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); - - if (compiledFile || switchMarkerAnalysis || sourceFile === this.bundle.src.file) { + if (decorationAnalyses.has(sourceFile) || switchMarkerAnalyses.has(sourceFile) || + sourceFile === this.bundle.src.file) { + const compiledFile = decorationAnalyses.get(sourceFile); + const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); renderedFiles.push(...this.renderFile( sourceFile, compiledFile, switchMarkerAnalysis, privateDeclarationsAnalyses)); } }); - // Transform the .d.ts files - if (this.bundle.dts) { - const dtsFiles = this.getTypingsFilesToRender( - decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); - - // If the dts entry-point is not already there (it did not have compiled classes) - // then add it now, to ensure it gets its extra exports rendered. - if (!dtsFiles.has(this.bundle.dts.file)) { - dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo()); - } - dtsFiles.forEach( - (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo))); - } - return renderedFiles; } @@ -127,142 +62,56 @@ export abstract class Renderer { renderFile( sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined, switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { - const input = this.extractSourceMap(sourceFile); + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] { + const isEntryPoint = sourceFile === this.bundle.src.file; + const input = extractSourceMap(this.fs, this.logger, sourceFile); const outputText = new MagicString(input.source); if (switchMarkerAnalysis) { - this.rewriteSwitchableDeclarations( + this.srcFormatter.rewriteSwitchableDeclarations( outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); } - if (compiledFile) { - const importManager = new ImportManager( - this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), - IMPORT_PREFIX); + const importManager = new ImportManager( + getImportRewriter(this.bundle.src.r3SymbolsFile, this.isCore, this.bundle.isFlatCore), + IMPORT_PREFIX); + if (compiledFile) { // TODO: remove constructor param metadata and property decorators (we need info from the // handlers to do this) const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses); - this.removeDecorators(outputText, decoratorsToRemove); + this.srcFormatter.removeDecorators(outputText, decoratorsToRemove); compiledFile.compiledClasses.forEach(clazz => { const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager); - this.addDefinitions(outputText, clazz, renderedDefinition); + this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition); }); - this.addConstants( + this.srcFormatter.addConstants( outputText, renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager), compiledFile.sourceFile); - - this.addImports( - outputText, importManager.getAllImports(compiledFile.sourceFile.fileName), - compiledFile.sourceFile); } // Add exports to the entry-point file - if (sourceFile === this.bundle.src.file) { + if (isEntryPoint) { const entryPointBasePath = stripExtension(this.bundle.src.path); - this.addExports(outputText, entryPointBasePath, privateDeclarationsAnalyses); + this.srcFormatter.addExports( + outputText, entryPointBasePath, privateDeclarationsAnalyses, importManager, sourceFile); } - return this.renderSourceAndMap(sourceFile, input, outputText); + if (isEntryPoint || compiledFile) { + this.srcFormatter.addImports( + outputText, importManager.getAllImports(sourceFile.fileName), sourceFile); + } + + if (compiledFile || switchMarkerAnalysis || isEntryPoint) { + return renderSourceAndMap(sourceFile, input, outputText); + } else { + return []; + } } - renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { - const input = this.extractSourceMap(dtsFile); - const outputText = new MagicString(input.source); - const printer = ts.createPrinter(); - const importManager = new ImportManager( - this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX); - - renderInfo.classInfo.forEach(dtsClass => { - const endOfClass = dtsClass.dtsDeclaration.getEnd(); - dtsClass.compilation.forEach(declaration => { - const type = translateType(declaration.type, importManager); - const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile); - const newStatement = ` static ${declaration.name}: ${typeStr};\n`; - outputText.appendRight(endOfClass - 1, newStatement); - }); - }); - - this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); - this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); - - this.addExports(outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports); - - - return this.renderSourceAndMap(dtsFile, input, outputText); - } - - /** - * Add the type parameters to the appropriate functions that return `ModuleWithProviders` - * structures. - * - * This function only gets called on typings files, so it doesn't need different implementations - * for each bundle format. - */ - protected addModuleWithProvidersParams( - outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], - importManager: ImportManager): void { - moduleWithProviders.forEach(info => { - const ngModuleName = info.ngModule.node.name.text; - const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile()); - const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile()); - const importPath = info.ngModule.viaModule || - (declarationFile !== ngModuleFile ? - stripExtension( - `./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) : - null); - const ngModule = getImportString(importManager, importPath, ngModuleName); - - if (info.declaration.type) { - const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ? - info.declaration.type.typeName : - null; - if (this.isCoreModuleWithProvidersType(typeName)) { - // The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type - // parameter adding. - outputText.overwrite( - info.declaration.type.getStart(), info.declaration.type.getEnd(), - `ModuleWithProviders<${ngModule}>`); - } else { - // The declaration returns an unknown type so we need to convert it to a union that - // includes the ngModule property. - const originalTypeString = info.declaration.type.getText(); - outputText.overwrite( - info.declaration.type.getStart(), info.declaration.type.getEnd(), - `(${originalTypeString})&{ngModule:${ngModule}}`); - } - } else { - // The declaration has no return type so provide one. - const lastToken = info.declaration.getLastToken(); - const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ? - lastToken.getStart() : - info.declaration.getEnd(); - outputText.appendLeft( - insertPoint, - `: ${getImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`); - } - }); - } - - protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): - void; - protected abstract addImports( - output: MagicString, imports: {specifier: string, qualifier: string}[], - sf: ts.SourceFile): void; - protected abstract addExports( - output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void; - protected abstract addDefinitions( - output: MagicString, compiledClass: CompiledClass, definitions: string): void; - protected abstract removeDecorators( - output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void; - protected abstract rewriteSwitchableDeclarations( - outputText: MagicString, sourceFile: ts.SourceFile, - declarations: SwitchableVariableDeclaration[]): void; - /** * From the given list of classes, computes a map of decorators that should be removed. * The decorators to remove are keyed by their container node, such that we can tell if @@ -270,7 +119,7 @@ export abstract class Renderer { * @param classes The list of classes that may have decorators to remove. * @returns A map of decorators to remove, keyed by their container node. */ - protected computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap { + private computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap { const decoratorsToRemove = new RedundantDecoratorMap(); classes.forEach(clazz => { clazz.decorators.forEach(dec => { @@ -284,191 +133,6 @@ export abstract class Renderer { }); return decoratorsToRemove; } - - /** - * Get the map from the source (note whether it is inline or external) - */ - protected extractSourceMap(file: ts.SourceFile): SourceMapInfo { - const inline = commentRegex.test(file.text); - const external = mapFileCommentRegex.exec(file.text); - - if (inline) { - const inlineSourceMap = fromSource(file.text); - return { - source: removeComments(file.text).replace(/\n\n$/, '\n'), - map: inlineSourceMap, - isInline: true, - }; - } else if (external) { - let externalSourceMap: SourceMapConverter|null = null; - try { - const fileName = external[1] || external[2]; - const filePath = AbsoluteFsPath.resolve( - AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); - const mappingFile = this.fs.readFile(filePath); - externalSourceMap = fromJSON(mappingFile); - } catch (e) { - if (e.code === 'ENOENT') { - this.logger.warn( - `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); - const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map'); - if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && - this.fs.stat(mapPath).isFile()) { - this.logger.warn( - `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); - try { - externalSourceMap = fromObject(JSON.parse(this.fs.readFile(mapPath))); - } catch (e) { - this.logger.error(e); - } - } - } - } - return { - source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'), - map: externalSourceMap, - isInline: false, - }; - } else { - return {source: file.text, map: null, isInline: false}; - } - } - - /** - * Merge the input and output source-maps, replacing the source-map comment in the output file - * with an appropriate source-map comment pointing to the merged source-map. - */ - protected renderSourceAndMap( - sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] { - const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); - const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); - const relativeSourcePath = PathSegment.basename(outputPath); - const relativeMapPath = `${relativeSourcePath}.map`; - - const outputMap = output.generateMap({ - source: outputPath, - includeContent: true, - // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix - // the merge algorithm. - }); - - // we must set this after generation as magic string does "manipulation" on the path - outputMap.file = relativeSourcePath; - - const mergedMap = - mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString())); - - const result: FileInfo[] = []; - if (input.isInline) { - result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}); - } else { - result.push({ - path: outputPath, - contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}` - }); - result.push({path: outputMapPath, contents: mergedMap.toJSON()}); - } - return result; - } - - protected getTypingsFilesToRender( - decorationAnalyses: DecorationAnalyses, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, - moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses| - null): Map { - const dtsMap = new Map(); - - // Capture the rendering info from the decoration analyses - decorationAnalyses.forEach(compiledFile => { - compiledFile.compiledClasses.forEach(compiledClass => { - const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); - if (dtsDeclaration) { - const dtsFile = dtsDeclaration.getSourceFile(); - const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo(); - renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation}); - dtsMap.set(dtsFile, renderInfo); - } - }); - }); - - // Capture the ModuleWithProviders functions/methods that need updating - if (moduleWithProvidersAnalyses !== null) { - moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => { - const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo(); - renderInfo.moduleWithProviders = moduleWithProvidersToFix; - dtsMap.set(dtsFile, renderInfo); - }); - } - - // Capture the private declarations that need to be re-exported - if (privateDeclarationsAnalyses.length) { - privateDeclarationsAnalyses.forEach(e => { - if (!e.dtsFrom && !e.alias) { - throw new Error( - `There is no typings path for ${e.identifier} in ${e.from}.\n` + - `We need to add an export for this class to a .d.ts typings file because ` + - `Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` + - `The simplest fix for this is to ensure that this class is exported from the package's entry-point.`); - } - }); - const dtsEntryPoint = this.bundle.dts !.file; - const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo(); - renderInfo.privateExports = privateDeclarationsAnalyses; - dtsMap.set(dtsEntryPoint, renderInfo); - } - - return dtsMap; - } - - /** - * Check whether the given type is the core Angular `ModuleWithProviders` interface. - * @param typeName The type to check. - * @returns true if the type is the core Angular `ModuleWithProviders` interface. - */ - private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) { - const id = - typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null; - return ( - id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core')); - } - - private getImportRewriter(r3SymbolsFile: ts.SourceFile|null, isFlat: boolean): ImportRewriter { - if (this.isCore && isFlat) { - return new NgccFlatImportRewriter(); - } else if (this.isCore) { - return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName); - } else { - return new NoopImportRewriter(); - } - } -} - -/** - * Merge the two specified source-maps into a single source-map that hides the intermediate - * source-map. - * E.g. Consider these mappings: - * - * ``` - * OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC - * ``` - * - * this will be replaced with: - * - * ``` - * OLD_SRC -> MERGED_MAP -> NEW_SRC - * ``` - */ -export function mergeSourceMaps( - oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter { - if (!oldMap) { - return fromObject(newMap); - } - const oldMapConsumer = new SourceMapConsumer(oldMap); - const newMapConsumer = new SourceMapConsumer(newMap); - const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer); - mergedMapGenerator.applySourceMap(oldMapConsumer); - const merged = fromJSON(mergedMapGenerator.toString()); - return merged; } /** @@ -476,7 +140,7 @@ export function mergeSourceMaps( */ export function renderConstantPool( sourceFile: ts.SourceFile, constantPool: ConstantPool, imports: ImportManager): string { - const printer = ts.createPrinter(); + const printer = createPrinter(); return constantPool.statements .map(stmt => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER)) .map(stmt => printer.printNode(ts.EmitHint.Unspecified, stmt, sourceFile)) @@ -493,7 +157,7 @@ export function renderConstantPool( */ export function renderDefinitions( sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string { - const printer = ts.createPrinter(); + const printer = createPrinter(); const name = compiledClass.declaration.name; const translate = (stmt: Statement) => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER); @@ -509,10 +173,6 @@ export function renderDefinitions( return definitions; } -export function stripExtension(filePath: T): T { - return filePath.replace(/\.(js|d\.ts)$/, '') as T; -} - /** * Create an Angular AST statement node that contains the assignment of the * compiled decorator to be applied to the class. @@ -524,8 +184,6 @@ function createAssignmentStatement( return new WritePropExpr(receiver, propName, initializer).toStmt(); } -function getImportString( - importManager: ImportManager, importPath: string | null, importName: string) { - const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null; - return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`; +function createPrinter(): ts.Printer { + return ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); } diff --git a/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts new file mode 100644 index 0000000000..b2a0ad2ca3 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer'; + +/** + * The collected decorators that have become redundant after the compilation + * of Ivy static fields. The map is keyed by the container node, such that we + * can tell if we should remove the entire decorator property + */ +export type RedundantDecoratorMap = Map; +export const RedundantDecoratorMap = Map; + +/** + * Implement this interface with methods that know how to render a specific format, + * such as ESM5 or UMD. + */ +export interface RenderingFormatter { + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void; + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void; + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void; + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void; + rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void; + addModuleWithProvidersParams( + outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts new file mode 100644 index 0000000000..23be4392c8 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright Google Inc. 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 {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; +import MagicString from 'magic-string'; +import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map'; +import * as ts from 'typescript'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {FileSystem} from '../file_system/file_system'; +import {Logger} from '../logging/logger'; +import {FileToWrite} from './utils'; + +export interface SourceMapInfo { + source: string; + map: SourceMapConverter|null; + isInline: boolean; +} + +/** + * Get the map from the source (note whether it is inline or external) + */ +export function extractSourceMap( + fs: FileSystem, logger: Logger, file: ts.SourceFile): SourceMapInfo { + const inline = commentRegex.test(file.text); + const external = mapFileCommentRegex.exec(file.text); + + if (inline) { + const inlineSourceMap = fromSource(file.text); + return { + source: removeComments(file.text).replace(/\n\n$/, '\n'), + map: inlineSourceMap, + isInline: true, + }; + } else if (external) { + let externalSourceMap: SourceMapConverter|null = null; + try { + const fileName = external[1] || external[2]; + const filePath = AbsoluteFsPath.resolve( + AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName); + const mappingFile = fs.readFile(filePath); + externalSourceMap = fromJSON(mappingFile); + } catch (e) { + if (e.code === 'ENOENT') { + logger.warn( + `The external map file specified in the source code comment "${e.path}" was not found on the file system.`); + const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map'); + if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && fs.exists(mapPath) && + fs.stat(mapPath).isFile()) { + logger.warn( + `Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`); + try { + externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath))); + } catch (e) { + logger.error(e); + } + } + } + } + return { + source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'), + map: externalSourceMap, + isInline: false, + }; + } else { + return {source: file.text, map: null, isInline: false}; + } +} + +/** + * Merge the input and output source-maps, replacing the source-map comment in the output file + * with an appropriate source-map comment pointing to the merged source-map. + */ +export function renderSourceAndMap( + sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] { + const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile); + const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`); + const relativeSourcePath = PathSegment.basename(outputPath); + const relativeMapPath = `${relativeSourcePath}.map`; + + const outputMap = output.generateMap({ + source: outputPath, + includeContent: true, + // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix + // the merge algorithm. + }); + + // we must set this after generation as magic string does "manipulation" on the path + outputMap.file = relativeSourcePath; + + const mergedMap = + mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString())); + + const result: FileToWrite[] = []; + if (input.isInline) { + result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}); + } else { + result.push({ + path: outputPath, + contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}` + }); + result.push({path: outputMapPath, contents: mergedMap.toJSON()}); + } + return result; +} + + +/** + * Merge the two specified source-maps into a single source-map that hides the intermediate + * source-map. + * E.g. Consider these mappings: + * + * ``` + * OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC + * ``` + * + * this will be replaced with: + * + * ``` + * OLD_SRC -> MERGED_MAP -> NEW_SRC + * ``` + */ +export function mergeSourceMaps( + oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter { + if (!oldMap) { + return fromObject(newMap); + } + const oldMapConsumer = new SourceMapConsumer(oldMap); + const newMapConsumer = new SourceMapConsumer(newMap); + const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer); + mergedMapGenerator.applySourceMap(oldMapConsumer); + const merged = fromJSON(mergedMapGenerator.toString()); + return merged; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts new file mode 100644 index 0000000000..f15e23b581 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts @@ -0,0 +1,232 @@ +/** + * @license + * Copyright Google Inc. 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 {dirname, relative} from 'canonical-path'; +import * as ts from 'typescript'; +import MagicString from 'magic-string'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {UmdReflectionHost} from '../host/umd_host'; +import {Esm5RenderingFormatter} from './esm5_rendering_formatter'; +import {stripExtension} from './utils'; + +type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; +type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; + +/** + * A RenderingFormatter that works with UMD files, instead of `import` and `export` statements + * the module is an IIFE with a factory function call with dependencies, which are defined in a + * wrapper function for AMD, CommonJS and global module formats. + */ +export class UmdRenderingFormatter extends Esm5RenderingFormatter { + constructor(protected umdHost: UmdReflectionHost, isCore: boolean) { super(umdHost, isCore); } + + /** + * Add the imports to the UMD module IIFE. + */ + addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { + // Assume there is only one UMD module in the file + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + + const wrapperFunction = umdModule.wrapperFn; + + // We need to add new `require()` calls for each import in the CommonJS initializer + renderCommonJsDependencies(output, wrapperFunction, imports); + renderAmdDependencies(output, wrapperFunction, imports); + renderGlobalDependencies(output, wrapperFunction, imports); + renderFactoryParameters(output, wrapperFunction, imports); + } + + /** + * Add the exports to the bottom of the UMD module factory function. + */ + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const lastStatement = + factoryFunction.body.statements[factoryFunction.body.statements.length - 1]; + const insertionPoint = + lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; + exports.forEach(e => { + const basePath = stripExtension(e.from); + const relativePath = './' + relative(dirname(entryPointBasePath), basePath); + const namedImport = entryPointBasePath !== basePath ? + importManager.generateNamedImport(relativePath, e.identifier) : + {symbol: e.identifier, moduleImport: null}; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; + output.appendRight(insertionPoint, exportStr); + }); + } + + /** + * Add the constants to the top of the UMD factory function. + */ + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const firstStatement = factoryFunction.body.statements[0]; + const insertionPoint = + firstStatement ? firstStatement.getStart() : factoryFunction.body.getStart() + 1; + output.appendLeft(insertionPoint, '\n' + constants + '\n'); + } +} + +/** + * Add dependencies to the CommonJS part of the UMD wrapper function. + */ +function renderCommonJsDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional); + if (!conditional) { + return; + } + const factoryCall = conditional.whenTrue; + const injectionPoint = factoryCall.getEnd() - + 1; // Backup one char to account for the closing parenthesis on the call + imports.forEach(i => output.appendLeft(injectionPoint, `,require('${i.specifier}')`)); +} + +/** + * Add dependencies to the AMD part of the UMD wrapper function. + */ +function renderAmdDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isAmdConditional); + if (!conditional) { + return; + } + const dependencyArray = conditional.whenTrue.arguments[1]; + if (!dependencyArray || !ts.isArrayLiteralExpression(dependencyArray)) { + return; + } + const injectionPoint = dependencyArray.getEnd() - + 1; // Backup one char to account for the closing square bracket on the array + imports.forEach(i => output.appendLeft(injectionPoint, `,'${i.specifier}'`)); +} + +/** + * Add dependencies to the global part of the UMD wrapper function. + */ +function renderGlobalDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall); + if (!globalFactoryCall) { + return; + } + // Backup one char to account for the closing parenthesis after the argument list of the call. + const injectionPoint = globalFactoryCall.getEnd() - 1; + imports.forEach(i => output.appendLeft(injectionPoint, `,global.${getGlobalIdentifier(i)}`)); +} + +/** + * Add dependency parameters to the UMD factory function. + */ +function renderFactoryParameters( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const wrapperCall = wrapperFunction.parent as ts.CallExpression; + const secondArgument = wrapperCall.arguments[1]; + if (!secondArgument) { + return; + } + + // Be resilient to the factory being inside parentheses + const factoryFunction = + ts.isParenthesizedExpression(secondArgument) ? secondArgument.expression : secondArgument; + if (!ts.isFunctionExpression(factoryFunction)) { + return; + } + const parameters = factoryFunction.parameters; + const injectionPoint = parameters[parameters.length - 1].getEnd(); + imports.forEach(i => output.appendLeft(injectionPoint, `,${i.qualifier}`)); +} + +/** + * Is this node the CommonJS conditional expression in the UMD wrapper? + */ +function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'exports', 'module'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'factory'; +} + +/** + * Is this node the AMD conditional expression in the UMD wrapper? + */ +function isAmdConditional(value: ts.Node): value is AmdConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'define'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'define'; +} + +/** + * Is this node the call to setup the global dependencies in the UMD wrapper? + */ +function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression { + if (ts.isCallExpression(value) && !!value.parent) { + // Be resilient to the value being inside parentheses + const expression = ts.isParenthesizedExpression(value.parent) ? value.parent : value; + return !!expression.parent && ts.isConditionalExpression(expression.parent) && + expression.parent.whenFalse === expression; + } else { + return false; + } +} + +function getGlobalIdentifier(i: Import) { + return i.specifier.replace('@angular/', 'ng.').replace(/^\//, ''); +} + +function find(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T|undefined { + return test(node) ? node : node.forEachChild(child => find(child, test)); +} + +function oneOfBinaryConditions( + node: ts.BinaryExpression, test: (expression: ts.Expression) => boolean) { + return test(node.left) || test(node.right); +} + +function isTypeOf(node: ts.Expression, ...types: string[]): boolean { + return ts.isBinaryExpression(node) && ts.isTypeOfExpression(node.left) && + ts.isIdentifier(node.left.expression) && types.indexOf(node.left.expression.text) !== -1; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/utils.ts b/packages/compiler-cli/ngcc/src/rendering/utils.ts new file mode 100644 index 0000000000..8392a09425 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/utils.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; +import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {NgccFlatImportRewriter} from './ngcc_import_rewriter'; + +/** + * Information about a file that has been rendered. + */ +export interface FileToWrite { + /** Path to where the file should be written. */ + path: AbsoluteFsPath; + /** The contents of the file to be be written. */ + contents: string; +} + +/** + * Create an appropriate ImportRewriter given the parameters. + */ +export function getImportRewriter( + r3SymbolsFile: ts.SourceFile | null, isCore: boolean, isFlat: boolean): ImportRewriter { + if (isCore && isFlat) { + return new NgccFlatImportRewriter(); + } else if (isCore) { + return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName); + } else { + return new NoopImportRewriter(); + } +} + +export function stripExtension(filePath: T): T { + return filePath.replace(/\.(js|d\.ts)$/, '') as T; +} diff --git a/packages/compiler-cli/ngcc/src/writing/file_writer.ts b/packages/compiler-cli/ngcc/src/writing/file_writer.ts index 0d7ee1e8a6..5b178e5355 100644 --- a/packages/compiler-cli/ngcc/src/writing/file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/file_writer.ts @@ -8,12 +8,13 @@ */ import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; /** * Responsible for writing out the transformed files to disk. */ export interface FileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]): void; + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]): + void; } diff --git a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts index 6bf43e94ca..adc7f396c5 100644 --- a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {FileWriter} from './file_writer'; /** @@ -20,11 +20,11 @@ import {FileWriter} from './file_writer'; export class InPlaceFileWriter implements FileWriter { constructor(protected fs: FileSystem) {} - writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { transformedFiles.forEach(file => this.writeFileAndBackup(file)); } - protected writeFileAndBackup(file: FileInfo): void { + protected writeFileAndBackup(file: FileToWrite): void { this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); if (this.fs.exists(backPath)) { diff --git a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts index 10414ebdfc..59ff4e67c1 100644 --- a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {InPlaceFileWriter} from './in_place_file_writer'; @@ -25,7 +25,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__'; * `InPlaceFileWriter`). */ export class NewEntryPointFileWriter extends InPlaceFileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { // The new folder is at the root of the overall package const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY); this.copyBundle(bundle, entryPoint.package, ngccFolder); @@ -47,7 +47,7 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { }); } - protected writeFile(file: FileInfo, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): + protected writeFile(file: FileToWrite, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): void { if (isDtsPath(file.path.replace(/\.map$/, ''))) { // This is either `.d.ts` or `.d.ts.map` file diff --git a/packages/compiler-cli/ngcc/test/BUILD.bazel b/packages/compiler-cli/ngcc/test/BUILD.bazel index 7aabc69b43..4ed0921b8d 100644 --- a/packages/compiler-cli/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/transform", + "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/test:test_utils", "@npm//@types/convert-source-map", "@npm//@types/mock-fs", diff --git a/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts new file mode 100644 index 0000000000..951fec0ef6 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/dependencies/commonjs_dependency_host_spec.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {CommonJsDependencyHost} from '../../src/dependencies/commonjs_dependency_host'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +const _ = AbsoluteFsPath.from; + +describe('CommonJsDependencyHost', () => { + let host: CommonJsDependencyHost; + beforeEach(() => { + const fs = createMockFileSystem(); + host = new CommonJsDependencyHost(fs, new ModuleResolver(fs)); + }); + + describe('getDependencies()', () => { + it('should not generate a TS AST if the source does not contain any require calls', () => { + spyOn(ts, 'createSourceFile'); + host.findDependencies(_('/no/imports/or/re-exports/index.js')); + expect(ts.createSourceFile).not.toHaveBeenCalled(); + }); + + it('should resolve all the external imports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should resolve all the external re-exports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/re-exports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should capture missing external imports', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports-missing/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(1); + expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); + expect(deepImports.size).toBe(0); + }); + + it('should not register deep imports as missing', () => { + // This scenario verifies the behavior of the dependency analysis when an external import + // is found that does not map to an entry-point but still exists on disk, i.e. a deep import. + // Such deep imports are captured for diagnostics purposes. + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/deep-import/index.js')); + + expect(dependencies.size).toBe(0); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(1); + expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true); + }); + + it('should recurse into internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/outer/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should handle circular internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/circular_a/index.js')); + expect(dependencies.size).toBe(2); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should support `paths` alias mappings when resolving modules', () => { + const fs = createMockFileSystem(); + host = new CommonJsDependencyHost(fs, new ModuleResolver(fs, { + baseUrl: '/dist', + paths: { + '@app/*': ['*'], + '@lib/*/test': ['lib/*/test'], + } + })); + const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); + expect(dependencies.size).toBe(4); + expect(dependencies.has(_('/dist/components'))).toBe(true); + expect(dependencies.has(_('/dist/shared'))).toBe(true); + expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + }); + + function createMockFileSystem() { + return new MockFileSystem({ + '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', + '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', + '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports/index.js': commonJs(['lib_1', 'lib_1/sub_1']), + '/external/imports/package.json': '{"esm2015": "./index.js"}', + '/external/imports/index.metadata.json': 'MOCK METADATA', + '/external/re-exports/index.js': + commonJs(['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']), + '/external/re-exports/package.json': '{"esm2015": "./index.js"}', + '/external/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports-missing/index.js': commonJs(['lib_1', 'missing']), + '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', + '/external/imports-missing/index.metadata.json': 'MOCK METADATA', + '/external/deep-import/index.js': commonJs(['lib_1/deep/import']), + '/external/deep-import/package.json': '{"esm2015": "./index.js"}', + '/external/deep-import/index.metadata.json': 'MOCK METADATA', + '/internal/outer/index.js': commonJs(['../inner']), + '/internal/outer/package.json': '{"esm2015": "./index.js"}', + '/internal/outer/index.metadata.json': 'MOCK METADATA', + '/internal/inner/index.js': commonJs(['lib_1/sub_1'], ['X']), + '/internal/circular_a/index.js': commonJs(['../circular_b', 'lib_1/sub_1'], ['Y']), + '/internal/circular_b/index.js': commonJs(['../circular_a', 'lib_1'], ['X']), + '/internal/circular_a/package.json': '{"esm2015": "./index.js"}', + '/internal/circular_a/index.metadata.json': 'MOCK METADATA', + '/re-directed/index.js': commonJs(['lib_1/sub_2']), + '/re-directed/package.json': '{"esm2015": "./index.js"}', + '/re-directed/index.metadata.json': 'MOCK METADATA', + '/path-alias/index.js': + commonJs(['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']), + '/path-alias/package.json': '{"esm2015": "./index.js"}', + '/path-alias/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/index.d.ts': 'export declare class X {}', + '/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}', + '/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}', + '/node_modules/lib_1/sub_1/package.json': + '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`, + '/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`, + '/node_modules/lib_1/sub_2/package.json': + '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}', + '/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA', + '/dist/components/index.d.ts': `export declare class MyComponent {};`, + '/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/components/index.metadata.json': 'MOCK METADATA', + '/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`, + '/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/shared/index.metadata.json': 'MOCK METADATA', + '/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`, + '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', + }); + } +}); + +function commonJs(importPaths: string[], exportNames: string[] = []) { + const commonJsRequires = + importPaths + .map( + p => + `var ${p.replace('@angular/', '').replace(/\.?\.?\//g, '').replace(/@/,'')} = require('${p}');`) + .join('\n'); + const exportStatements = + exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); + return `${commonJsRequires} +${exportStatements}`; +} diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index 65e42dcb93..733e9ccccf 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -9,6 +9,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {DependencyResolver, SortedEntryPointsInfo} from '../../src/dependencies/dependency_resolver'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {FileSystem} from '../../src/file_system/file_system'; import {EntryPoint} from '../../src/packages/entry_point'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; @@ -18,10 +19,13 @@ const _ = AbsoluteFsPath.from; describe('DependencyResolver', () => { let host: EsmDependencyHost; let resolver: DependencyResolver; + let fs: FileSystem; + let moduleResolver: ModuleResolver; beforeEach(() => { - const fs = new MockFileSystem(); - host = new EsmDependencyHost(fs, new ModuleResolver(fs)); - resolver = new DependencyResolver(new MockLogger(), host); + fs = new MockFileSystem(); + moduleResolver = new ModuleResolver(fs); + host = new EsmDependencyHost(fs, moduleResolver); + resolver = new DependencyResolver(fs, new MockLogger(), {esm5: host, esm2015: host}); }); describe('sortEntryPointsByDependency()', () => { const first = { @@ -106,10 +110,17 @@ describe('DependencyResolver', () => { ]); }); - it('should error if the entry point does not have either the esm5 nor esm2015 formats', () => { + it('should error if the entry point does not have a suitable format', () => { expect(() => resolver.sortEntryPointsByDependency([ { path: '/first', packageJson: {}, compiledByAngular: true } as EntryPoint - ])).toThrowError(`There is no format with import statements in '/first' entry-point.`); + ])).toThrowError(`There is no appropriate source code format in '/first' entry-point.`); + }); + + it('should error if there is no appropriate DependencyHost for the given formats', () => { + resolver = new DependencyResolver(fs, new MockLogger(), {esm2015: host}); + expect(() => resolver.sortEntryPointsByDependency([first])) + .toThrowError( + `Could not find a suitable format for computing dependencies of entry-point: '${first.path}'.`); }); it('should capture any dependencies that were ignored', () => { @@ -138,6 +149,30 @@ describe('DependencyResolver', () => { expect(sorted.entryPoints).toEqual([fifth]); }); + it('should use the appropriate DependencyHost for each entry-point', () => { + const esm5Host = new EsmDependencyHost(fs, moduleResolver); + const esm2015Host = new EsmDependencyHost(fs, moduleResolver); + resolver = + new DependencyResolver(fs, new MockLogger(), {esm5: esm5Host, esm2015: esm2015Host}); + spyOn(esm5Host, 'findDependencies').and.callFake(createFakeComputeDependencies(dependencies)); + spyOn(esm2015Host, 'findDependencies') + .and.callFake(createFakeComputeDependencies(dependencies)); + const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]); + expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]); + + expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${first.path}/index.js`); + expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${second.path}/sub/index.js`); + expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${third.path}/index.js`); + expect(esm5Host.findDependencies).not.toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); + expect(esm5Host.findDependencies).toHaveBeenCalledWith(`${fifth.path}/index.js`); + + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${first.path}/index.js`); + expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${second.path}/sub/index.js`); + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${third.path}/index.js`); + expect(esm2015Host.findDependencies).toHaveBeenCalledWith(`${fourth.path}/sub2/index.js`); + expect(esm2015Host.findDependencies).not.toHaveBeenCalledWith(`${fifth.path}/index.js`); + }); + interface DepMap { [path: string]: {resolved: string[], missing: string[]}; } diff --git a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts index bea7dc250f..6298ba3f2c 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/esm_dependency_host_spec.ts @@ -7,14 +7,14 @@ */ import * as ts from 'typescript'; -import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {EsmDependencyHost} from '../../src/dependencies/esm_dependency_host'; import {ModuleResolver} from '../../src/dependencies/module_resolver'; import {MockFileSystem} from '../helpers/mock_file_system'; const _ = AbsoluteFsPath.from; -describe('DependencyHost', () => { +describe('EsmDependencyHost', () => { let host: EsmDependencyHost; beforeEach(() => { const fs = createMockFileSystem(); @@ -56,7 +56,7 @@ describe('DependencyHost', () => { expect(dependencies.size).toBe(1); expect(dependencies.has(_('/node_modules/lib-1'))).toBe(true); expect(missing.size).toBe(1); - expect(missing.has('missing')).toBe(true); + expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); expect(deepImports.size).toBe(0); }); @@ -70,7 +70,7 @@ describe('DependencyHost', () => { expect(dependencies.size).toBe(0); expect(missing.size).toBe(0); expect(deepImports.size).toBe(1); - expect(deepImports.has('/node_modules/lib-1/deep/import')).toBe(true); + expect(deepImports.has(_('/node_modules/lib-1/deep/import'))).toBe(true); }); it('should recurse into internal dependencies', () => { diff --git a/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts new file mode 100644 index 0000000000..521af669b8 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/dependencies/umd_dependency_host_spec.ts @@ -0,0 +1,192 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; +import {ModuleResolver} from '../../src/dependencies/module_resolver'; +import {UmdDependencyHost} from '../../src/dependencies/umd_dependency_host'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +const _ = AbsoluteFsPath.from; + +describe('UmdDependencyHost', () => { + let host: UmdDependencyHost; + beforeEach(() => { + const fs = createMockFileSystem(); + host = new UmdDependencyHost(fs, new ModuleResolver(fs)); + }); + + describe('getDependencies()', () => { + it('should not generate a TS AST if the source does not contain any require calls', () => { + spyOn(ts, 'createSourceFile'); + host.findDependencies(_('/no/imports/or/re-exports/index.js')); + expect(ts.createSourceFile).not.toHaveBeenCalled(); + }); + + it('should resolve all the external imports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should resolve all the external re-exports of the source file', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/re-exports/index.js')); + expect(dependencies.size).toBe(2); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + }); + + it('should capture missing external imports', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/imports-missing/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(1); + expect(missing.has(PathSegment.fromFsPath('missing'))).toBe(true); + expect(deepImports.size).toBe(0); + }); + + it('should not register deep imports as missing', () => { + // This scenario verifies the behavior of the dependency analysis when an external import + // is found that does not map to an entry-point but still exists on disk, i.e. a deep import. + // Such deep imports are captured for diagnostics purposes. + const {dependencies, missing, deepImports} = + host.findDependencies(_('/external/deep-import/index.js')); + + expect(dependencies.size).toBe(0); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(1); + expect(deepImports.has(_('/node_modules/lib_1/deep/import'))).toBe(true); + }); + + it('should recurse into internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/outer/index.js')); + + expect(dependencies.size).toBe(1); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should handle circular internal dependencies', () => { + const {dependencies, missing, deepImports} = + host.findDependencies(_('/internal/circular_a/index.js')); + expect(dependencies.size).toBe(2); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1/sub_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + + it('should support `paths` alias mappings when resolving modules', () => { + const fs = createMockFileSystem(); + host = new UmdDependencyHost(fs, new ModuleResolver(fs, { + baseUrl: '/dist', + paths: { + '@app/*': ['*'], + '@lib/*/test': ['lib/*/test'], + } + })); + const {dependencies, missing, deepImports} = host.findDependencies(_('/path-alias/index.js')); + expect(dependencies.size).toBe(4); + expect(dependencies.has(_('/dist/components'))).toBe(true); + expect(dependencies.has(_('/dist/shared'))).toBe(true); + expect(dependencies.has(_('/dist/lib/shared/test'))).toBe(true); + expect(dependencies.has(_('/node_modules/lib_1'))).toBe(true); + expect(missing.size).toBe(0); + expect(deepImports.size).toBe(0); + }); + }); + + function createMockFileSystem() { + return new MockFileSystem({ + '/no/imports/or/re-exports/index.js': '// some text but no import-like statements', + '/no/imports/or/re-exports/package.json': '{"esm2015": "./index.js"}', + '/no/imports/or/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports/index.js': umd('imports_index', ['lib_1', 'lib_1/sub_1']), + '/external/imports/package.json': '{"esm2015": "./index.js"}', + '/external/imports/index.metadata.json': 'MOCK METADATA', + '/external/re-exports/index.js': + umd('imports_index', ['lib_1', 'lib_1/sub_1'], ['lib_1.X', 'lib_1sub_1.Y']), + '/external/re-exports/package.json': '{"esm2015": "./index.js"}', + '/external/re-exports/index.metadata.json': 'MOCK METADATA', + '/external/imports-missing/index.js': umd('imports_missing', ['lib_1', 'missing']), + '/external/imports-missing/package.json': '{"esm2015": "./index.js"}', + '/external/imports-missing/index.metadata.json': 'MOCK METADATA', + '/external/deep-import/index.js': umd('deep_import', ['lib_1/deep/import']), + '/external/deep-import/package.json': '{"esm2015": "./index.js"}', + '/external/deep-import/index.metadata.json': 'MOCK METADATA', + '/internal/outer/index.js': umd('outer', ['../inner']), + '/internal/outer/package.json': '{"esm2015": "./index.js"}', + '/internal/outer/index.metadata.json': 'MOCK METADATA', + '/internal/inner/index.js': umd('inner', ['lib_1/sub_1'], ['X']), + '/internal/circular_a/index.js': umd('circular_a', ['../circular_b', 'lib_1/sub_1'], ['Y']), + '/internal/circular_b/index.js': umd('circular_b', ['../circular_a', 'lib_1'], ['X']), + '/internal/circular_a/package.json': '{"esm2015": "./index.js"}', + '/internal/circular_a/index.metadata.json': 'MOCK METADATA', + '/re-directed/index.js': umd('re_directed', ['lib_1/sub_2']), + '/re-directed/package.json': '{"esm2015": "./index.js"}', + '/re-directed/index.metadata.json': 'MOCK METADATA', + '/path-alias/index.js': + umd('path_alias', ['@app/components', '@app/shared', '@lib/shared/test', 'lib_1']), + '/path-alias/package.json': '{"esm2015": "./index.js"}', + '/path-alias/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/index.d.ts': 'export declare class X {}', + '/node_modules/lib_1/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/deep/import/index.js': 'export class DeepImport {}', + '/node_modules/lib_1/sub_1/index.d.ts': 'export declare class Y {}', + '/node_modules/lib_1/sub_1/package.json': + '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/node_modules/lib_1/sub_1/index.metadata.json': 'MOCK METADATA', + '/node_modules/lib_1/sub_2.d.ts': `export * from './sub_2/sub_2';`, + '/node_modules/lib_1/sub_2/sub_2.d.ts': `export declare class Z {}';`, + '/node_modules/lib_1/sub_2/package.json': + '{"esm2015": "./sub_2.js", "typings": "./sub_2.d.ts"}', + '/node_modules/lib_1/sub_2/sub_2.metadata.json': 'MOCK METADATA', + '/dist/components/index.d.ts': `export declare class MyComponent {};`, + '/dist/components/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/components/index.metadata.json': 'MOCK METADATA', + '/dist/shared/index.d.ts': `import {X} from 'lib_1';\nexport declare class Service {}`, + '/dist/shared/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/shared/index.metadata.json': 'MOCK METADATA', + '/dist/lib/shared/test/index.d.ts': `export class TestHelper {}`, + '/dist/lib/shared/test/package.json': '{"esm2015": "./index.js", "typings": "./index.d.ts"}', + '/dist/lib/shared/test/index.metadata.json': 'MOCK METADATA', + }); + } +}); + +function umd(moduleName: string, importPaths: string[], exportNames: string[] = []) { + const commonJsRequires = importPaths.map(p => `,require('${p}')`).join(''); + const amdDeps = importPaths.map(p => `,'${p}'`).join(''); + const globalParams = + importPaths.map(p => `,global.${p.replace('@angular/', 'ng.').replace(/\//g, '')}`).join(''); + const params = + importPaths.map(p => `,${p.replace('@angular/', '').replace(/\.?\.?\//g, '')}`).join(''); + const exportStatements = + exportNames.map(e => ` exports.${e.replace(/.+\./, '')} = ${e};`).join('\n'); + return ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports${commonJsRequires}) : + typeof define === 'function' && define.amd ? define('${moduleName}', ['exports'${amdDeps}], factory) : + (factory(global.${moduleName}${globalParams})); +}(this, (function (exports${params}) { 'use strict'; +${exportStatements} +}))); + `; +} diff --git a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel index 012a925b94..cc237359fe 100644 --- a/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel +++ b/packages/compiler-cli/ngcc/test/helpers/BUILD.bazel @@ -12,5 +12,6 @@ ts_library( "//packages/compiler-cli/ngcc", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/testing", + "@npm//typescript", ], ) diff --git a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts index c47e2f359e..3e8fd219b4 100644 --- a/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts +++ b/packages/compiler-cli/ngcc/test/helpers/mock_file_system.ts @@ -14,7 +14,7 @@ import {FileStats, FileSystem} from '../../src/file_system/file_system'; export class MockFileSystem implements FileSystem { files: Folder = {}; constructor(...folders: Folder[]) { - folders.forEach(files => this.processFiles(this.files, files)); + folders.forEach(files => this.processFiles(this.files, files, true)); } exists(path: AbsoluteFsPath): boolean { return this.findFromPath(path) !== null; } @@ -67,7 +67,7 @@ export class MockFileSystem implements FileSystem { return new MockFileStats(fileOrFolder); } - pwd(): AbsoluteFsPath { return AbsoluteFsPath.fromUnchecked('/'); } + pwd(): AbsoluteFsPath { return AbsoluteFsPath.from('/'); } copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { this.writeFile(to, this.readFile(from)); @@ -82,9 +82,10 @@ export class MockFileSystem implements FileSystem { ensureDir(path: AbsoluteFsPath): void { this.ensureFolders(this.files, path.split('/')); } - private processFiles(current: Folder, files: Folder): void { + private processFiles(current: Folder, files: Folder, isRootPath = false): void { Object.keys(files).forEach(path => { - const segments = path.split('/'); + const pathResolved = isRootPath ? AbsoluteFsPath.from(path) : path; + const segments = pathResolved.split('/'); const lastSegment = segments.pop() !; const containingFolder = this.ensureFolders(current, segments); const entity = files[path]; diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index f4eae77e54..5463251b46 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -64,49 +64,38 @@ export function makeTestProgram( // TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package export function getFakeCore() { return { - name: 'node_modules/@angular/core/index.ts', + name: 'node_modules/@angular/core/index.d.ts', contents: ` type FnWithArg = (arg?: any) => T; - function callableClassDecorator(): FnWithArg<(clazz: any) => any> { - return null !; + export declare const Component: FnWithArg<(clazz: any) => any>; + export declare const Directive: FnWithArg<(clazz: any) => any>; + export declare const Injectable: FnWithArg<(clazz: any) => any>; + export declare const NgModule: FnWithArg<(clazz: any) => any>; + + export declare const Input: any; + + export declare const Inject: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const Self: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const SkipSelf: FnWithArg<(a: any, b: any, c: any) => void>; + export declare const Optional: FnWithArg<(a: any, b: any, c: any) => void>; + + export declare class InjectionToken { + constructor(name: string); } - function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> { - return null !; - } - - function makePropDecorator(): any { - } - - export const Component = callableClassDecorator(); - export const Directive = callableClassDecorator(); - export const Injectable = callableClassDecorator(); - export const NgModule = callableClassDecorator(); - - export const Input = makePropDecorator(); - - export const Inject = callableParamDecorator(); - export const Self = callableParamDecorator(); - export const SkipSelf = callableParamDecorator(); - export const Optional = callableParamDecorator(); - - export class InjectionToken { - constructor(name: string) {} - } - - export interface ModuleWithProviders {} + export declare interface ModuleWithProviders {} ` }; } export function getFakeTslib() { return { - name: 'node_modules/tslib/index.ts', + name: 'node_modules/tslib/index.d.ts', contents: ` - export function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any) {} - export function __param(paramIndex: number, decorator: any) {} - export function __metadata(metadataKey: any, metadataValue: any) {} + export declare function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any); + export declare function __param(paramIndex: number, decorator: any); + export declare function __metadata(metadataKey: any, metadataValue: any); ` }; } diff --git a/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts new file mode 100644 index 0000000000..9d02cba996 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/host/commonjs_host_spec.ts @@ -0,0 +1,1766 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {getIifeBody} from '../../src/host/esm5_host'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestBundleProgram} from '../helpers/utils'; + +import {expectTypeValueReferencesForParameters} from './util'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.cjs.js', + contents: ` +var core = require('@angular/core'); + +var INJECTED_TOKEN = new InjectionToken('injected'); +var ViewContainerRef = {}; +var TemplateRef = {}; + +var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + SomeDirective.prototype = { + instanceMethod: function() {}, + }; + SomeDirective.staticMethod = function() {}; + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: core.Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: core.Input },], + "input2": [{ type: core.Input },], + }; + return SomeDirective; +}()); +exports.SomeDirective = SomeDirective; +` +}; + +const SIMPLE_ES2015_CLASS_FILE = { + name: '/simple_es2015_class.d.ts', + contents: ` + export class EmptyClass {} + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` +var EmptyClass = (function() { + function EmptyClass() { + } + return EmptyClass; +}()); +var NoDecoratorConstructorClass = (function() { + function NoDecoratorConstructorClass(foo) { + } + return NoDecoratorConstructorClass; +}()); +exports.EmptyClass = EmptyClass; +exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; +`, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` +var core = require('@angular/core'); +function foo() {} +foo.decorators = [ + { type: core.Directive, args: [{ selector: '[ignored]' },] } +]; +exports.foo = foo; +`, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` +var core = require('@angular/core'); +var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = () => [ + { type: core.Directive, args: [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; +}()); + +var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: core.Directive }, + ]; + return NotObjectLiteral; +}()); + +var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.decorators = [ + { notType: core.Directive }, + { type: core.Directive }, + ]; + return NoTypeProperty; +}()); + +var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ]; + return NotIdentifier; +}()); +`, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` +var core = require('@angular/core'); +var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.decorators = [ + { type: core.Directive }, + ]; + return NoArgsProperty; +}()); + +var args = [{ selector: '[ignored]' },]; +var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.decorators = [ + { type: core.Directive, args }, + ]; + return NoPropertyAssignment; +}()); + +var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = [ + { type: core.Directive, args: () => [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; +}()); +`, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` +var core = require('@angular/core'); +var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: core.Directive },] + }); + return NotObjectLiteral; +}()); + +var NotObjectLiteralProp = (function() { + function NotObjectLiteralProp() { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: core.Directive }, + ] + }; + return NotObjectLiteralProp; +}()); + +var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: core.Directive }, + { type: core.Directive }, + ] + }; + return NoTypeProperty; +}()); + +var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ] + }; + return NotIdentifier; +}()); +`, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` +var core = require('@angular/core'); +var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: core.Input },] + }; + return NoArgsProperty; +}()); + +var args = [{ selector: '[ignored]' },]; +var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: core.Input, args },] + }; + return NoPropertyAssignment; +}()); + +var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: core.Input, args: () => [{ selector: '[ignored]' },] },], + }; + return NotArrayLiteral; +}()); +`, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` +var core = require('@angular/core'); +var NoParameters = (function() { + function NoParameters() {} + return NoParameters; +}()); + +var ArrowFunction = (function() { + function ArrowFunction(arg1) { + } + ArrowFunction.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: core.Inject },] } + ]; + return ArrowFunction; +}()); + +var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return 'StringsAreNotArrayLiterals'; }; + return NotArrayLiteral; +}()); + +var NotObjectLiteral = (function() { + function NotObjectLiteral(arg1, arg2) { + } + NotObjectLiteral.ctorParameters = function() { return [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NotObjectLiteral; +}()); + +var NoTypeProperty = (function() { + function NoTypeProperty(arg1, arg2) { + } + NoTypeProperty.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { notType: core.Inject }, + { type: core.Inject }, + ] + }, + ]; }; + return NoTypeProperty; +}()); + +var NotIdentifier = (function() { + function NotIdentifier(arg1, arg2) { + } + NotIdentifier.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Inject }, + ] + }, + ]; }; + return NotIdentifier; +}()); +`, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` +var core = require('@angular/core'); +var NoArgsProperty = (function() { + function NoArgsProperty(arg1) { + } + NoArgsProperty.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NoArgsProperty; +}()); + +var args = [{ selector: '[ignored]' },]; +var NoPropertyAssignment = (function() { + function NoPropertyAssignment(arg1) { + } + NoPropertyAssignment.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args },] }, + ]; }; + return NoPropertyAssignment; +}()); + +var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args: () => [{ selector: '[ignored]' },] },] }, + ]; }; + return NotArrayLiteral; +}()); +`, +}; + +const IMPORTS_FILES = [ + { + name: '/file_a.js', + contents: ` +var a = 'a'; +exports.a = a; +`, + }, + { + name: '/file_b.js', + contents: ` +var file_a = require('./file_a'); +var b = file_a.a; +var c = 'c'; +var d = c; +`, + }, + { + name: '/file_c.js', + contents: ` +var file_a = require('./file_a'); +var c = file_a.a; +`, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a_module.js', + contents: ` +var a = 'a'; +exports.a = a; +`, + }, + { + name: '/b_module.js', + contents: ` +var core = require('@angular/core'); +var a_module = require('/a_module'); +var b = a_module.a; +var e = 'e'; +var SomeClass = (function() { + function SomeClass() {} + return SomeClass; +}()); + +exports.Directive = core.Directive; +exports.a = a_module.a; +exports.b = b; +exports.c = a_module.a; +exports.d = b; +exports.e = e; +exports.DirectiveX = core.Directive; +exports.SomeClass = SomeClass; +`, + }, + { + name: '/xtra_module.js', + contents: ` +var xtra1 = 'xtra1'; +var xtra2 = 'xtra2'; +exports.xtra1 = xtra1; +exports.xtra2 = xtra2; +`, + }, + { + name: '/wildcard_reexports.js', + contents: ` + function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + } + __export(require("./b_module")); + __export(require("./xtra_module")); + ` + }, +]; + +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` +function foo(x) { + return x; +} +function bar(x, y) { + if (y === void 0) { y = 42; } + return x + y; +} +function complex() { + var x = 42; + return 42; +} +function baz(x) { + var y; + if (x === void 0) { y = 42; } + return y; +} +var y; +function qux(x) { + if (x === void 0) { y = 42; } + return y; +} +function moo() { + var x; + if (x === void 0) { x = 42; } + return x; +} +var x; +function juu() { + if (x === void 0) { x = 42; } + return x; +} +` +}; + +const DECORATED_FILES = [ + { + name: '/primary.js', + contents: ` +var core = require('@angular/core'); +var secondary = require('./secondary'); +var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] } + ]; + return A; +}()); + var B = (function() { + function B() {} + B.decorators = [ + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + function x() {} + function y() {} + var C = (function() { + function C() {} + return C; +}); +exports.A = A; +exports.x = x; +exports.C = C; +` + }, + { + name: '/secondary.js', + contents: ` +var core = require('@angular/core'); +var D = (function() { + function D() {} + D.decorators = [ + { type: core.Directive, args: [{ selector: '[d]' }] } + ]; + return D; +}()); +exports.D = D; + ` + } +]; + +const TYPINGS_SRC_FILES = [ + { + name: '/src/index.js', + contents: ` +var internal = require('./internal'); +var class1 = require('./class1'); +var class2 = require('./class2'); +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +var InternalClass = internal.InternalClass; +__export(class1); +__export(class2); +` + }, + { + name: '/src/class1.js', + contents: ` +var Class1 = (function() { + function Class1() {} + return Class1; +}()); +var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; +}()); +exports.Class1 = Class1; +exports.MissingClass1 = MissingClass1; +` + }, + { + name: '/src/class2.js', + contents: ` +var Class2 = (function() { + function Class2() {} + return Class2; +}()); +exports.Class2 = Class2; +` + }, + {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { + name: '/src/internal.js', + contents: ` +var InternalClass = (function() { + function InternalClass() {} + return InternalClass; +}()); +var Class2 = (function() { + function Class2() {} + return Class2; +}()); +exports.InternalClass =InternalClass; +exports.Class2 = Class2; +` + }, + { + name: '/src/missing-class.js', + contents: ` +var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; +}()); +exports. MissingClass2 = MissingClass2; +` + }, + { + name: '/src/flat-file.js', + contents: ` +var Class1 = (function() { + function Class1() {} + return Class1; +}()); +var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; +}()); +var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; +}()); +var Class3 = (function() { + function Class3() {} + return Class3; +}()); +exports.Class1 = Class1; +exports.xClass3 = Class3; +exports.MissingClass1 = MissingClass1; +exports.MissingClass2 = MissingClass2; +` + } +]; + +const TYPINGS_DTS_FILES = [ + { + name: '/typings/index.d.ts', + contents: + `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` + }, + { + name: '/typings/class1.d.ts', + contents: `export declare class Class1 {}\nexport declare class OtherClass {}` + }, + { + name: '/typings/class2.d.ts', + contents: + `export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';` + }, + {name: '/typings/func1.d.ts', contents: 'export declare function mooFn(): void;'}, + { + name: '/typings/internal.d.ts', + contents: `export declare class InternalClass {}\nexport declare class Class2 {}` + }, + {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, +]; + +const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/functions.js', + contents: ` +var mod = require('./module'); +var SomeService = (function() { + function SomeService() {} + return SomeService; +}()); + +var InternalModule = (function() { + function InternalModule() {} + return InternalModule; +}()); + +function aNumber() { return 42; } +function aString() { return 'foo'; } +function emptyObject() { return {}; } +function ngModuleIdentifier() { return { ngModule: InternalModule }; } +function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } +function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } +function onlyProviders() { return { providers: [SomeService] }; } +function ngModuleNumber() { return { ngModule: 42 }; } +function ngModuleString() { return { ngModule: 'foo' }; } +function ngModuleObject() { return { ngModule: { foo: 42 } }; } +function externalNgModule() { return { ngModule: mod.ExternalModule }; } +// NOTE: We do not include the "namespaced" export tests in CommonJS as all CommonJS exports are already namespaced. +// function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } + +exports.aNumber = aNumber; +exports.aString = aString; +exports.emptyObject = emptyObject; +exports.ngModuleIdentifier = ngModuleIdentifier; +exports.ngModuleWithEmptyProviders = ngModuleWithEmptyProviders; +exports.ngModuleWithProviders = ngModuleWithProviders; +exports.onlyProviders = onlyProviders; +exports.ngModuleNumber = ngModuleNumber; +exports.ngModuleString = ngModuleString; +exports.ngModuleObject = ngModuleObject; +exports.externalNgModule = externalNgModule; +exports.SomeService = SomeService; +exports.InternalModule = InternalModule; +` + }, + { + name: '/src/methods.js', + contents: ` +var mod = require('./module'); +var SomeService = (function() { + function SomeService() {} + return SomeService; +}()); + +var InternalModule = (function() { + function InternalModule() {} + InternalModule.prototype = { + instanceNgModuleIdentifier: function() { return { ngModule: InternalModule }; }, + instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, + instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, + instanceExternalNgModule: function() { return { ngModule: mod.ExternalModule }; }, + }; + InternalModule.aNumber = function() { return 42; }; + InternalModule.aString = function() { return 'foo'; }; + InternalModule.emptyObject = function() { return {}; }; + InternalModule.ngModuleIdentifier = function() { return { ngModule: InternalModule }; }; + InternalModule.ngModuleWithEmptyProviders = function() { return { ngModule: InternalModule, providers: [] }; }; + InternalModule.ngModuleWithProviders = function() { return { ngModule: InternalModule, providers: [SomeService] }; }; + InternalModule.onlyProviders = function() { return { providers: [SomeService] }; }; + InternalModule.ngModuleNumber = function() { return { ngModule: 42 }; }; + InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; + InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; + InternalModule.externalNgModule = function() { return { ngModule: mod.ExternalModule }; }; + return InternalModule; +}()); + +exports.SomeService = SomeService; +exports.InternalModule = InternalModule; +` + }, + { + name: '/src/aliased_class.js', + contents: ` +var AliasedModule = (function() { + function AliasedModule() {} + AliasedModule_1 = AliasedModule; + AliasedModule.forRoot = function() { return { ngModule: AliasedModule_1 }; }; + var AliasedModule_1; + return AliasedModule; +}()); +exports.AliasedModule = AliasedModule; +` + }, + { + name: '/src/module.js', + contents: ` +var ExternalModule = (function() { + function ExternalModule() {} + return ExternalModule; +}()); +exports.ExternalModule = ExternalModule; +` + }, +]; + + +describe('CommonJsReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + + it('should return null if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + // In ES5 there is no such thing as a constructor-less class + // it('should return `null` if there is no constructor', () => { }); + + it('should return an array even if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + // In ES5 there are no arrow functions + // it('should ignore `ctorParameters` if it is an arrow function', () => { }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(CommonJsReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FUNCTION_BODY_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', isNamedFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', isNamedFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', isNamedFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', isNamedFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + }); + }); + + describe('getImportOfIdentifier', () => { + it('should find the import of an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'b', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + + it('should return null if the identifier was not imported', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'd', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + + it('should handle factory functions not wrapped in parentheses', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_c.js', 'c', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier, + defaultImportStatement: null, + }).expression; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', isNamedVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the source-file of an import namespace', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const {program, host: compilerHost} = makeTestBundleProgram(EXPORTS_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/b_module.js') !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.entries()) + .map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule])) + .toEqual([ + ['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + ['a', `a = 'a'`, '/a_module'], + ['b', `b = a_module.a`, null], + ['c', `a = 'a'`, '/a_module'], + ['d', `b = a_module.a`, null], + ['e', `e = 'e'`, null], + ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + [ + 'SomeClass', + `SomeClass = (function() {\n function SomeClass() {}\n return SomeClass;\n}())`, + null + ], + ]); + }); + + it('should handle wildcard re-exports of other modules', () => { + const {program, host: compilerHost} = makeTestBundleProgram(EXPORTS_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/wildcard_reexports.js') !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.entries()) + .map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule])) + .toEqual([ + ['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '/b_module'], + ['a', `a = 'a'`, '/b_module'], + ['b', `b = a_module.a`, '/b_module'], + ['c', `a = 'a'`, '/b_module'], + ['d', `b = a_module.a`, '/b_module'], + ['e', `e = 'e'`, '/b_module'], + ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, '/b_module'], + [ + 'SomeClass', + `SomeClass = (function() {\n function SomeClass() {}\n return SomeClass;\n}())`, + '/b_module' + ], + ['xtra1', `xtra1 = 'xtra1'`, '/xtra_module'], + ['xtra2', `xtra2 = 'xtra2'`, '/xtra_module'], + ]); + }); + }); + + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_ES2015_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (inner function declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(innerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(outerNode); + }); + + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', + () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + + expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + }); + + it('should return undefined if node is not an ES5 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); + }); + + describe('isClass()', () => { + let host: CommonJsReflectionHost; + let mockNode: ts.Node; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; + + beforeEach(() => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + mockNode = {} as any; + + getClassDeclarationSpy = spyOn(CommonJsReflectionHost.prototype, 'getClassDeclaration'); + superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + }); + + it('should return true if superclass returns true', () => { + superGetClassDeclarationSpy.and.returnValue(true); + getClassDeclarationSpy.and.callThrough(); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return true if it can find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(true); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return false if it cannot find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(false); + + expect(host.isClass(mockNode)).toBe(false); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + }); + + describe('hasBaseClass()', () => { + function hasBaseClass(source: string) { + const file = { + name: '/synthesized_constructors.js', + contents: source, + }; + + const {program, host: compilerHost} = makeTestBundleProgram([file]); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + return host.hasBaseClass(classNode); + } + + it('should consider an IIFE with _super parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should consider an IIFE with a unique name generated for the _super parameter as having a base class', + () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should not consider an IIFE without parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(false); + }); + }); + + describe('findDecoratedClasses()', () => { + it('should return an array of all decorated classes in the given source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const primary = program.getSourceFile(DECORATED_FILES[0].name) !; + + const primaryDecoratedClasses = host.findDecoratedClasses(primary); + expect(primaryDecoratedClasses.length).toEqual(2); + const classA = primaryDecoratedClasses.find(c => c.name === 'A') !; + expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + // Note that `B` is not exported from `primary.js` + const classB = primaryDecoratedClasses.find(c => c.name === 'B') !; + expect(classB.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + + const secondary = program.getSourceFile(DECORATED_FILES[1].name) !; + const secondaryDecoratedClasses = host.findDecoratedClasses(secondary); + expect(secondaryDecoratedClasses.length).toEqual(1); + // Note that `D` is exported from `secondary.js` but not exported from `primary.js` + const classD = secondaryDecoratedClasses.find(c => c.name === 'D') !; + expect(classD.name).toEqual('D'); + expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + }); + }); + + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = getDeclaration(program, '/src/class1.js', 'Class1', ts.isVariableDeclaration); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find the dts declaration for exported functions', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES); + const mooFn = getDeclaration(program, '/src/func1.js', 'mooFn', ts.isFunctionDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dtsProgram); + + const dtsDeclaration = host.getDtsDeclaration(mooFn); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); + }); + + it('should return null if there is no matching class in the matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = + getDeclaration(program, '/src/class1.js', 'MissingClass1', ts.isVariableDeclaration); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should return null if there is no matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = getDeclaration( + program, '/src/missing-class.js', 'MissingClass2', ts.isVariableDeclaration); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should find the dts file that contains a matching class declaration, even if the source files do not match', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = + getDeclaration(program, '/src/flat-file.js', 'Class1', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find aliased exports', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class3 = + getDeclaration(program, '/src/flat-file.js', 'Class3', ts.isVariableDeclaration); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class3); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts'); + }); + + it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const internalClass = + getDeclaration(program, '/src/internal.js', 'InternalClass', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(internalClass); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts'); + }); + + it('should prefer the publicly exported class if there are multiple classes with the same name', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class2 = + getDeclaration(program, '/src/class2.js', 'Class2', ts.isVariableDeclaration); + const internalClass2 = + getDeclaration(program, '/src/internal.js', 'Class2', ts.isVariableDeclaration); + const host = + new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const class2DtsDeclaration = host.getDtsDeclaration(class2); + expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts'); + + const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2); + expect(internalClass2DtsDeclaration !.getSourceFile().fileName) + .toEqual('/typings/class2.d.ts'); + }); + }); + + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/functions.js') !; + const fns = host.getModuleWithProvidersFunctions(file); + expect(fns.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) + .toEqual([ + ['ngModuleIdentifier', 'InternalModule'], + ['ngModuleWithEmptyProviders', 'InternalModule'], + ['ngModuleWithProviders', 'InternalModule'], + ['externalNgModule', 'ExternalModule'], + ]); + }); + + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/methods.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + [ + 'function() { return { ngModule: InternalModule }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [SomeService] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: mod.ExternalModule }; }', + 'ExternalModule', + ], + ]); + }); + + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/aliased_class.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + ['function() { return { ngModule: AliasedModule_1 }; }', 'AliasedModule'], + ]); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts index da5834541a..44d6315a52 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_import_helper_spec.ts @@ -346,7 +346,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { null; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', + program, 'node_modules/@angular/core/index.d.ts', 'Directive', isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); expect(actualDeclaration).not.toBe(null); diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index 98e20267ff..75defb0600 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -521,6 +521,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/functions.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; export class SomeService {} export class InternalModule {} export function aNumber() { return 42; } @@ -534,12 +535,14 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function ngModuleString() { return { ngModule: 'foo' }; } export function ngModuleObject() { return { ngModule: { foo: 42 } }; } export function externalNgModule() { return { ngModule: ExternalModule }; } + export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } ` }, { name: '/src/methods.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; export class SomeService {} export class InternalModule { static aNumber() { return 42; } @@ -553,11 +556,13 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ static ngModuleString() { return { ngModule: 'foo' }; } static ngModuleObject() { return { ngModule: { foo: 42 } }; } static externalNgModule() { return { ngModule: ExternalModule }; } + static namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } instanceNgModuleIdentifier() { return { ngModule: InternalModule }; } instanceNgModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } instanceNgModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } instanceExternalNgModule() { return { ngModule: ExternalModule }; } + instanceNamespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } } ` }, @@ -574,6 +579,19 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ {name: '/src/module.js', contents: 'export class ExternalModule {}'}, ]; +const NAMESPACED_IMPORT_FILE = { + name: '/some_directive.js', + contents: ` + import * as core from '@angular/core'; + + class SomeDirective { + } + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + ` +}; + describe('Esm2015ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1341,13 +1359,33 @@ describe('Esm2015ReflectionHost', () => { .initializer as ts.Identifier; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', isNamedVariableDeclaration); + program, 'node_modules/@angular/core/index.d.ts', 'Directive', + isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); expect(actualDeclaration).not.toBe(null); expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + it('should return the source-file of an import namespace', () => { + const {program} = makeTestBundleProgram([NAMESPACED_IMPORT_FILE]); + const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, NAMESPACED_IMPORT_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifier = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifier); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + it('should return the original declaration of an aliased class', () => { const program = makeTestProgram(CLASS_EXPRESSION_FILE); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); @@ -1382,9 +1420,7 @@ describe('Esm2015ReflectionHost', () => { const values = Array.from(exportDeclarations !.values()) .map(declaration => [declaration.node.getText(), declaration.viaModule]); expect(values).toEqual([ - // TODO clarify what is expected here... - // [`Directive = callableClassDecorator()`, '@angular/core'], - [`Directive = callableClassDecorator()`, null], + [`Directive: FnWithArg<(clazz: any) => any>`, null], [`a = 'a'`, null], [`b = a`, null], [`c = foo`, null], @@ -1649,6 +1685,7 @@ describe('Esm2015ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); @@ -1665,6 +1702,7 @@ describe('Esm2015ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts index bcaadcc8a9..65b02de6ff 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -365,7 +365,7 @@ describe('Esm5ReflectionHost [import helper style]', () => { null; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', + program, 'node_modules/@angular/core/index.d.ts', 'Directive', isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !); expect(actualDeclaration).not.toBe(null); diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 657b299b31..76cc48b156 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -402,7 +402,7 @@ const IMPORTS_FILES = [ { name: '/a.js', contents: ` - export const a = 'a'; + export var a = 'a'; `, }, { @@ -422,7 +422,7 @@ const EXPORTS_FILES = [ { name: '/a.js', contents: ` - export const a = 'a'; + export var a = 'a'; `, }, { @@ -643,6 +643,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/functions.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; var SomeService = (function() { function SomeService() {} @@ -664,6 +665,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ export function ngModuleString() { return { ngModule: 'foo' }; } export function ngModuleObject() { return { ngModule: { foo: 42 } }; } export function externalNgModule() { return { ngModule: ExternalModule }; } + export function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } export {SomeService, InternalModule}; ` }, @@ -671,6 +673,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ name: '/src/methods.js', contents: ` import {ExternalModule} from './module'; + import * as mod from './module'; var SomeService = (function() { function SomeService() {} return SomeService; @@ -683,6 +686,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, instanceExternalNgModule: function() { return { ngModule: ExternalModule }; }, + namespacedExternalNgModule = function() { return { ngModule: mod.ExternalModule }; }, }; InternalModule.aNumber = function() { return 42; }; InternalModule.aString = function() { return 'foo'; }; @@ -695,6 +699,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; InternalModule.externalNgModule = function() { return { ngModule: ExternalModule }; }; + InternalModule.namespacedExternalNgModule = function() { return { ngModule: mod.ExternalModule }; }; return InternalModule; }()); export {SomeService, InternalModule}; @@ -716,6 +721,22 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ {name: '/src/module.js', contents: 'export class ExternalModule {}'}, ]; +const NAMESPACED_IMPORT_FILE = { + name: '/some_directive.js', + contents: ` + import * as core from '@angular/core'; + + var SomeDirective = (function() { + function SomeDirective() { + } + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + return SomeDirective; + }()); + ` +}; + describe('Esm5ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1493,13 +1514,33 @@ describe('Esm5ReflectionHost', () => { .initializer as ts.Identifier; const expectedDeclarationNode = getDeclaration( - program, 'node_modules/@angular/core/index.ts', 'Directive', isNamedVariableDeclaration); + program, 'node_modules/@angular/core/index.d.ts', 'Directive', + isNamedVariableDeclaration); const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); expect(actualDeclaration).not.toBe(null); expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + it('should return the source-file of an import namespace', () => { + const program = makeTestProgram(NAMESPACED_IMPORT_FILE); + const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); + const classNode = getDeclaration( + program, NAMESPACED_IMPORT_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifier = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifier); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + it('should return the correct declaration for an inner function identifier inside an ES5 IIFE', () => { const superGetDeclarationOfIdentifierSpy = @@ -1550,9 +1591,7 @@ describe('Esm5ReflectionHost', () => { const values = Array.from(exportDeclarations !.values()) .map(declaration => [declaration.node.getText(), declaration.viaModule]); expect(values).toEqual([ - // TODO: clarify what is expected here... - //[`Directive = callableClassDecorator()`, '@angular/core'], - [`Directive = callableClassDecorator()`, null], + [`Directive: FnWithArg<(clazz: any) => any>`, null], [`a = 'a'`, null], [`b = a`, null], [`c = foo`, null], @@ -1851,6 +1890,7 @@ describe('Esm5ReflectionHost', () => { ['ngModuleWithEmptyProviders', 'InternalModule'], ['ngModuleWithProviders', 'InternalModule'], ['externalNgModule', 'ExternalModule'], + ['namespacedExternalNgModule', 'ExternalModule'], ]); }); @@ -1877,6 +1917,10 @@ describe('Esm5ReflectionHost', () => { 'function() { return { ngModule: ExternalModule }; }', 'ExternalModule', ], + [ + 'function() { return { ngModule: mod.ExternalModule }; }', + 'ExternalModule', + ], ]); }); diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts new file mode 100644 index 0000000000..bf76f5a5d9 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -0,0 +1,1857 @@ +/** + * @license + * Copyright Google Inc. 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 * as ts from 'typescript'; + +import {ClassMemberKind, CtorParameter, Import, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {getIifeBody} from '../../src/host/esm5_host'; +import {UmdReflectionHost} from '../../src/host/umd_host'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestBundleProgram} from '../helpers/utils'; + +import {expectTypeValueReferencesForParameters} from './util'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.umd.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('some_directive', ['exports', '@angular/core'], factory) : + (factory(global.some_directive,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = {}; + var TemplateRef = {}; + + var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + SomeDirective.prototype = { + instanceMethod: function() {}, + }; + SomeDirective.staticMethod = function() {}; + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: core.Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: core.Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: core.Input },], + "input2": [{ type: core.Input },], + }; + return SomeDirective; + }()); + exports.SomeDirective = SomeDirective; +})));`, +}; + +const SIMPLE_ES2015_CLASS_FILE = { + name: '/simple_es2015_class.d.ts', + contents: ` + export class EmptyClass {} + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('simple_class', ['exports'], factory) : + (factory(global.simple_class)); +}(this, (function (exports) { 'use strict'; + var EmptyClass = (function() { + function EmptyClass() { + } + return EmptyClass; + }()); + var NoDecoratorConstructorClass = (function() { + function NoDecoratorConstructorClass(foo) { + } + return NoDecoratorConstructorClass; + }()); + exports.EmptyClass = EmptyClass; + exports.NoDecoratorConstructorClass = NoDecoratorConstructorClass; +})));`, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('foo_function', ['exports', '@angular/core'], factory) : + (factory(global.foo_function,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + function foo() {} + foo.decorators = [ + { type: core.Directive, args: [{ selector: '[ignored]' },] } + ]; + exports.foo = foo; +})));`, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_decorators, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = () => [ + { type: core.Directive, args: [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); + + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: core.Directive }, + ]; + return NotObjectLiteral; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.decorators = [ + { notType: core.Directive }, + { type: core.Directive }, + ]; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ]; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_decorator_args, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.decorators = [ + { type: core.Directive }, + ]; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.decorators = [ + { type: core.Directive, args }, + ]; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = [ + { type: core.Directive, args: () => [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); +})));`, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_prop_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_prop_decorators, global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: core.Directive },] + }); + return NotObjectLiteral; + }()); + + var NotObjectLiteralProp = (function() { + function NotObjectLiteralProp() { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: core.Directive }, + ] + }; + return NotObjectLiteralProp; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: core.Directive }, + { type: core.Directive }, + ] + }; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Directive }, + ] + }; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_prop_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_prop_decorator_args, global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: core.Input },] + }; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: core.Input, args },] + }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: core.Input, args: () => [{ selector: '[ignored]' },] },], + }; + return NotArrayLiteral; + }()); +})));`, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_ctor_decorators', ['exports', '@angular/core'], factory) : + (factory(global.invalid_ctor_decorators,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoParameters = (function() { + function NoParameters() {} + return NoParameters; + }()); + + var ArrowFunction = (function() { + function ArrowFunction(arg1) { + } + ArrowFunction.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: core.Inject },] } + ]; + return ArrowFunction; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return 'StringsAreNotArrayLiterals'; }; + return NotArrayLiteral; + }()); + + var NotObjectLiteral = (function() { + function NotObjectLiteral(arg1, arg2) { + } + NotObjectLiteral.ctorParameters = function() { return [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NotObjectLiteral; + }()); + + var NoTypeProperty = (function() { + function NoTypeProperty(arg1, arg2) { + } + NoTypeProperty.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { notType: core.Inject }, + { type: core.Inject }, + ] + }, + ]; }; + return NoTypeProperty; + }()); + + var NotIdentifier = (function() { + function NotIdentifier(arg1, arg2) { + } + NotIdentifier.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: core.Inject }, + ] + }, + ]; }; + return NotIdentifier; + }()); +})));`, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('invalid_ctor_decorator_args', ['exports', '@angular/core'], factory) : + (factory(global.invalid_ctor_decorator_args,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var NoArgsProperty = (function() { + function NoArgsProperty(arg1) { + } + NoArgsProperty.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject },] }, + ]; }; + return NoArgsProperty; + }()); + + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment(arg1) { + } + NoPropertyAssignment.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args },] }, + ]; }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: core.Inject, args: () => [{ selector: '[ignored]' },] },] }, + ]; }; + return NotArrayLiteral; + }()); +})));`, +}; + +const IMPORTS_FILES = [ + { + name: '/file_a.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('file_a', ['exports'], factory) : + (factory(global.file_a)); +}(this, (function (exports) { 'use strict'; + var a = 'a'; + exports.a = a; +})));`, + }, + { + name: '/file_b.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./file_a')) : + typeof define === 'function' && define.amd ? define('file_b', ['exports', './file_a'], factory) : + (factory(global.file_b,global.file_a)); +}(this, (function (exports, file_a) { 'use strict'; + var b = file_a.a; + var c = 'c'; + var d = c; +})));`, + }, + { + name: '/file_c.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./file_a')) : + typeof define === 'function' && define.amd ? define('file_c', ['exports', 'file_a'], factory) : + (factory(global.file_c,global.file_a)); +}(this, function (exports, file_a) { 'use strict'; + var c = file_a.a; +}));`, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a_module.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('a_module', ['exports'], factory) : + (factory(global.a_module)); +}(this, (function (exports) { 'use strict'; + var a = 'a'; + exports.a = a; +})));`, + }, + { + name: '/b_module.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('/a_module')) : + typeof define === 'function' && define.amd ? define('b_module', ['exports', '@angular/core', 'a_module'], factory) : + (factory(global.b_module)); +}(this, (function (exports, core, a_module) { 'use strict'; + var b = a_module.a; + var e = 'e'; + var SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }()); + + exports.Directive = core.Directive; + exports.a = a_module.a; + exports.b = b; + exports.c = a_module.a; + exports.d = b; + exports.e = e; + exports.DirectiveX = core.Directive; + exports.SomeClass = SomeClass; +})));`, + }, +]; + +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('function_body', ['exports'], factory) : + (factory(global.function_body)); +}(this, (function (exports) { 'use strict'; + function foo(x) { + return x; + } + function bar(x, y) { + if (y === void 0) { y = 42; } + return x + y; + } + function complex() { + var x = 42; + return 42; + } + function baz(x) { + var y; + if (x === void 0) { y = 42; } + return y; + } + var y; + function qux(x) { + if (x === void 0) { y = 42; } + return y; + } + function moo() { + var x; + if (x === void 0) { x = 42; } + return x; + } + var x; + function juu() { + if (x === void 0) { x = 42; } + return x; + } +})));` +}; + +const DECORATED_FILES = [ + { + name: '/primary.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('./secondary')) : + typeof define === 'function' && define.amd ? define('primary', ['exports', '@angular/core', './secondary'], factory) : + (factory(global.primary,global.ng.core, global.secondary)); + }(this, (function (exports,core,secondary) { 'use strict'; + var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] } + ]; + return A; + }()); + var B = (function() { + function B() {} + B.decorators = [ + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; + }()); + function x() {} + function y() {} + var C = (function() { + function C() {} + return C; + }); + exports.A = A; + exports.x = x; + exports.C = C; + })));` + }, + { + name: '/secondary.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('primary', ['exports', '@angular/core'], factory) : + (factory(global.primary,global.ng.core)); + }(this, (function (exports,core) { 'use strict'; + var D = (function() { + function D() {} + D.decorators = [ + { type: core.Directive, args: [{ selector: '[d]' }] } + ]; + return D; + }()); + exports.D = D; + }))); + ` + } +]; + +const TYPINGS_SRC_FILES = [ + { + name: '/src/index.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./internal'), require('./class1'), require('./class2')) : + typeof define === 'function' && define.amd ? define('index', ['exports', './internal', './class1', './class2'], factory) : + (factory(global.index,global.internal,global.class1,global.class2)); + }(this, (function (exports,internal,class1,class2) { 'use strict'; + function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + } + var InternalClass = internal.InternalClass; + __export(class1); + __export(class2); + }))); + ` + }, + { + name: '/src/class1.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('class1', ['exports'], factory) : + (factory(global.class1)); + }(this, (function (exports) { 'use strict'; + var Class1 = (function() { + function Class1() {} + return Class1; + }()); + var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; + }()); + exports.Class1 = Class1; + exports.MissingClass1 = MissingClass1; + }))); + ` + }, + { + name: '/src/class2.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('class2', ['exports'], factory) : + (factory(global.class2)); + }(this, (function (exports) { 'use strict'; + var Class2 = (function() { + function Class2() {} + return Class2; + }()); + exports.Class2 = Class2; + }))); + ` + }, + {name: '/src/func1.js', contents: 'function mooFn() {} export {mooFn}'}, { + name: '/src/internal.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('internal', ['exports'], factory) : + (factory(global.internal)); + }(this, (function (exports) { 'use strict'; + var InternalClass = (function() { + function InternalClass() {} + return InternalClass; + }()); + var Class2 = (function() { + function Class2() {} + return Class2; + }()); + exports.InternalClass =InternalClass; + exports.Class2 = Class2; + }))); + ` + }, + { + name: '/src/missing-class.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('missingClass', ['exports'], factory) : + (factory(global.missingClass)); + }(this, (function (exports) { 'use strict'; + var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; + }()); + exports. MissingClass2 = MissingClass2; + }))); + ` + }, + { + name: '/src/flat-file.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('missingClass', ['exports'], factory) : + (factory(global.missingClass)); + }(this, (function (exports) { 'use strict'; + var Class1 = (function() { + function Class1() {} + return Class1; + }()); + var MissingClass1 = (function() { + function MissingClass1() {} + return MissingClass1; + }()); + var MissingClass2 = (function() { + function MissingClass2() {} + return MissingClass2; + }()); + var Class3 = (function() { + function Class3() {} + return Class3; + }()); + exports.Class1 = Class1; + exports.xClass3 = Class3; + exports.MissingClass1 = MissingClass1; + exports.MissingClass2 = MissingClass2; + }))); + ` + } +]; + +const TYPINGS_DTS_FILES = [ + { + name: '/typings/index.d.ts', + contents: + `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` + }, + { + name: '/typings/class1.d.ts', + contents: `export declare class Class1 {}\nexport declare class OtherClass {}` + }, + { + name: '/typings/class2.d.ts', + contents: + `export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';` + }, + {name: '/typings/func1.d.ts', contents: 'export declare function mooFn(): void;'}, + { + name: '/typings/internal.d.ts', + contents: `export declare class InternalClass {}\nexport declare class Class2 {}` + }, + {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, +]; + +const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/functions.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./module')) : + typeof define === 'function' && define.amd ? define('functions', ['exports', './module'], factory) : + (factory(global.functions,global.module)); + }(this, (function (exports,module) { 'use strict'; + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + return InternalModule; + }()); + + function aNumber() { return 42; } + function aString() { return 'foo'; } + function emptyObject() { return {}; } + function ngModuleIdentifier() { return { ngModule: InternalModule }; } + function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } + function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } + function onlyProviders() { return { providers: [SomeService] }; } + function ngModuleNumber() { return { ngModule: 42 }; } + function ngModuleString() { return { ngModule: 'foo' }; } + function ngModuleObject() { return { ngModule: { foo: 42 } }; } + function externalNgModule() { return { ngModule: module.ExternalModule }; } + // NOTE: We do not include the "namespaced" export tests in UMD as all UMD exports are already namespaced. + // function namespacedExternalNgModule() { return { ngModule: mod.ExternalModule }; } + + exports.aNumber = aNumber; + exports.aString = aString; + exports.emptyObject = emptyObject; + exports.ngModuleIdentifier = ngModuleIdentifier; + exports.ngModuleWithEmptyProviders = ngModuleWithEmptyProviders; + exports.ngModuleWithProviders = ngModuleWithProviders; + exports.onlyProviders = onlyProviders; + exports.ngModuleNumber = ngModuleNumber; + exports.ngModuleString = ngModuleString; + exports.ngModuleObject = ngModuleObject; + exports.externalNgModule = externalNgModule; + exports.SomeService = SomeService; + exports.InternalModule = InternalModule; + }))); + ` + }, + { + name: '/src/methods.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('./module')) : + typeof define === 'function' && define.amd ? define('methods', ['exports', './module'], factory) : + (factory(global.methods,global.module)); + }(this, (function (exports,module) { 'use strict'; + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + InternalModule.prototype = { + instanceNgModuleIdentifier: function() { return { ngModule: InternalModule }; }, + instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, + instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, + instanceExternalNgModule: function() { return { ngModule: module.ExternalModule }; }, + }; + InternalModule.aNumber = function() { return 42; }; + InternalModule.aString = function() { return 'foo'; }; + InternalModule.emptyObject = function() { return {}; }; + InternalModule.ngModuleIdentifier = function() { return { ngModule: InternalModule }; }; + InternalModule.ngModuleWithEmptyProviders = function() { return { ngModule: InternalModule, providers: [] }; }; + InternalModule.ngModuleWithProviders = function() { return { ngModule: InternalModule, providers: [SomeService] }; }; + InternalModule.onlyProviders = function() { return { providers: [SomeService] }; }; + InternalModule.ngModuleNumber = function() { return { ngModule: 42 }; }; + InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; + InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; + InternalModule.externalNgModule = function() { return { ngModule: module.ExternalModule }; }; + return InternalModule; + }()); + + exports.SomeService = SomeService; + exports.InternalModule = InternalModule; + }))); + ` + }, + { + name: '/src/aliased_class.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('aliased_class', ['exports'], factory) : + (factory(global.aliased_class)); + }(this, (function (exports,module) { 'use strict'; + var AliasedModule = (function() { + function AliasedModule() {} + AliasedModule_1 = AliasedModule; + AliasedModule.forRoot = function() { return { ngModule: AliasedModule_1 }; }; + var AliasedModule_1; + return AliasedModule; + }()); + exports.AliasedModule = AliasedModule; + }))); + ` + }, + { + name: '/src/module.js', + contents: ` + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define('module', ['exports'], factory) : + (factory(global.module)); + }(this, (function (exports,module) { 'use strict'; + var ExternalModule = (function() { + function ExternalModule() {} + return ExternalModule; + }()); + exports.ExternalModule = ExternalModule; + }))); + ` + }, +]; + + +describe('UmdReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + + it('should return null if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Directive'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_PROP_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import; + const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo); + + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_PROP_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Input'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expectTypeValueReferencesForParameters(parameters !, [ + 'ViewContainerRef', + 'TemplateRef', + null, + ]); + }); + + it('should throw if the symbol is not a class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + // In ES5 there is no such thing as a constructor-less class + // it('should return `null` if there is no constructor', () => { }); + + it('should return an array even if there are no decorators', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + // In ES5 there are no arrow functions + // it('should ignore `ctorParameters` if it is an arrow function', () => { }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([INVALID_CTOR_DECORATORS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'}; + const spy = spyOn(UmdReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const {program, host: compilerHost} = + makeTestBundleProgram([INVALID_CTOR_DECORATOR_ARGS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + isNamedVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('Inject'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FUNCTION_BODY_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', isNamedFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', isNamedFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', isNamedFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', isNamedFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + }); + }); + + describe('getImportOfIdentifier', () => { + it('should find the import of an identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'b', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + + it('should return null if the identifier was not imported', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_b.js', 'd', isNamedVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + + it('should handle factory functions not wrapped in parentheses', () => { + const {program, host: compilerHost} = makeTestBundleProgram(IMPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const variableNode = getDeclaration(program, '/file_c.js', 'c', isNamedVariableDeclaration); + const identifier = + (variableNode.initializer && ts.isPropertyAccessExpression(variableNode.initializer)) ? + variableNode.initializer.name : + null; + + expect(identifier).not.toBe(null); + const importOfIdent = host.getImportOfIdentifier(identifier !); + expect(importOfIdent).toEqual({name: 'a', from: './file_a'}); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{ + local: true, + expression: ts.Identifier, + defaultImportStatement: null, + }).expression; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', isNamedVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the source-file of an import namespace', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SOME_DIRECTIVE_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = (((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.PropertyAccessExpression) + .expression as ts.Identifier; + + const expectedDeclarationNode = + program.getSourceFile('node_modules/@angular/core/index.d.ts') !; + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const {program, host: compilerHost} = makeTestBundleProgram(EXPORTS_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile(EXPORTS_FILES[1].name) !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.entries()) + .map(entry => [entry[0], entry[1].node.getText(), entry[1].viaModule])) + .toEqual([ + ['Directive', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + ['a', `a = 'a'`, '/a_module'], + ['b', `b = a_module.a`, null], + ['c', `a = 'a'`, '/a_module'], + ['d', `b = a_module.a`, null], + ['e', `e = 'e'`, null], + ['DirectiveX', `Directive: FnWithArg<(clazz: any) => any>`, '@angular/core'], + [ + 'SomeClass', `SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }())`, + null + ], + ]); + }); + + // Currently we do not support UMD versions of `export * from 'x';` + // because it gets compiled to something like: + // + // __export(m) { + // for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; + // } + // __export(x); + // + // So far all UMD formatted entry-points are flat so this should not occur. + // If it does later then we should implement parsing. + }); + + describe('getClassSymbol()', () => { + it('should return the class symbol for an ES2015 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_ES2015_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = getDeclaration( + program, SIMPLE_ES2015_CLASS_FILE.name, 'EmptyClass', isNamedClassDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (outer variable declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(node); + }); + + it('should return the class symbol for an ES5 class (inner function declaration)', () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + const classSymbol = host.getClassSymbol(innerNode); + + expect(classSymbol).toBeDefined(); + expect(classSymbol !.valueDeclaration).toBe(outerNode); + }); + + it('should return the same class symbol (of the outer declaration) for outer and inner declarations', + () => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', isNamedVariableDeclaration); + const innerNode = getIifeBody(outerNode) !.statements.find(isNamedFunctionDeclaration) !; + + expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + }); + + it('should return undefined if node is not an ES5 class', () => { + const {program, host: compilerHost} = makeTestBundleProgram([FOO_FUNCTION_FILE]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const node = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', isNamedFunctionDeclaration); + const classSymbol = host.getClassSymbol(node); + + expect(classSymbol).toBeUndefined(); + }); + }); + + describe('isClass()', () => { + let host: UmdReflectionHost; + let mockNode: ts.Node; + let getClassDeclarationSpy: jasmine.Spy; + let superGetClassDeclarationSpy: jasmine.Spy; + + beforeEach(() => { + const {program, host: compilerHost} = makeTestBundleProgram([SIMPLE_CLASS_FILE]); + host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + mockNode = {} as any; + + getClassDeclarationSpy = spyOn(UmdReflectionHost.prototype, 'getClassDeclaration'); + superGetClassDeclarationSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassDeclaration'); + }); + + it('should return true if superclass returns true', () => { + superGetClassDeclarationSpy.and.returnValue(true); + getClassDeclarationSpy.and.callThrough(); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + expect(superGetClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return true if it can find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(true); + + expect(host.isClass(mockNode)).toBe(true); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + + it('should return false if it cannot find a declaration for the class', () => { + getClassDeclarationSpy.and.returnValue(false); + + expect(host.isClass(mockNode)).toBe(false); + expect(getClassDeclarationSpy).toHaveBeenCalledWith(mockNode); + }); + }); + + describe('hasBaseClass()', () => { + function hasBaseClass(source: string) { + const file = { + name: '/synthesized_constructors.js', + contents: source, + }; + + const {program, host: compilerHost} = makeTestBundleProgram([file]); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const classNode = getDeclaration(program, file.name, 'TestClass', isNamedVariableDeclaration); + return host.hasBaseClass(classNode); + } + + it('should consider an IIFE with _super parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super) { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should consider an IIFE with a unique name generated for the _super parameter as having a base class', + () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function (_super_1) { + __extends(TestClass, _super_1); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(true); + }); + + it('should not consider an IIFE without parameter as having a base class', () => { + const result = hasBaseClass(` + var TestClass = /** @class */ (function () { + __extends(TestClass, _super); + function TestClass() {} + return TestClass; + }(null));`); + expect(result).toBe(false); + }); + }); + + describe('findDecoratedClasses()', () => { + it('should return an array of all decorated classes in the given source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(DECORATED_FILES); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const primary = program.getSourceFile(DECORATED_FILES[0].name) !; + + const primaryDecoratedClasses = host.findDecoratedClasses(primary); + expect(primaryDecoratedClasses.length).toEqual(2); + const classA = primaryDecoratedClasses.find(c => c.name === 'A') !; + expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + // Note that `B` is not exported from `primary.js` + const classB = primaryDecoratedClasses.find(c => c.name === 'B') !; + expect(classB.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + + const secondary = program.getSourceFile(DECORATED_FILES[1].name) !; + const secondaryDecoratedClasses = host.findDecoratedClasses(secondary); + expect(secondaryDecoratedClasses.length).toEqual(1); + // Note that `D` is exported from `secondary.js` but not exported from `primary.js` + const classD = secondaryDecoratedClasses.find(c => c.name === 'D') !; + expect(classD.name).toEqual('D'); + expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + }); + }); + + describe('getDtsDeclarationsOfClass()', () => { + it('should find the dts declaration that has the same relative path to the source file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = getDeclaration(program, '/src/class1.js', 'Class1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find the dts declaration for exported functions', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dtsProgram = makeTestBundleProgram(TYPINGS_DTS_FILES); + const mooFn = getDeclaration(program, '/src/func1.js', 'mooFn', ts.isFunctionDeclaration); + const host = + new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dtsProgram); + + const dtsDeclaration = host.getDtsDeclaration(mooFn); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/func1.d.ts'); + }); + + it('should return null if there is no matching class in the matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = + getDeclaration(program, '/src/class1.js', 'MissingClass1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should return null if there is no matching dts file', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const missingClass = getDeclaration( + program, '/src/missing-class.js', 'MissingClass2', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + expect(host.getDtsDeclaration(missingClass)).toBe(null); + }); + + it('should find the dts file that contains a matching class declaration, even if the source files do not match', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class1 = + getDeclaration(program, '/src/flat-file.js', 'Class1', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class1); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts'); + }); + + it('should find aliased exports', () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class3 = + getDeclaration(program, '/src/flat-file.js', 'Class3', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(class3); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts'); + }); + + it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const internalClass = + getDeclaration(program, '/src/internal.js', 'InternalClass', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const dtsDeclaration = host.getDtsDeclaration(internalClass); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts'); + }); + + it('should prefer the publicly exported class if there are multiple classes with the same name', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(TYPINGS_SRC_FILES); + const dts = makeTestBundleProgram(TYPINGS_DTS_FILES); + const class2 = + getDeclaration(program, '/src/class2.js', 'Class2', ts.isVariableDeclaration); + const internalClass2 = + getDeclaration(program, '/src/internal.js', 'Class2', ts.isVariableDeclaration); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost, dts); + + const class2DtsDeclaration = host.getDtsDeclaration(class2); + expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts'); + + const internalClass2DtsDeclaration = host.getDtsDeclaration(internalClass2); + expect(internalClass2DtsDeclaration !.getSourceFile().fileName) + .toEqual('/typings/class2.d.ts'); + }); + }); + + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/functions.js') !; + const fns = host.getModuleWithProvidersFunctions(file); + expect(fns.map(fn => [fn.declaration.name !.getText(), fn.ngModule.node.name.text])) + .toEqual([ + ['ngModuleIdentifier', 'InternalModule'], + ['ngModuleWithEmptyProviders', 'InternalModule'], + ['ngModuleWithProviders', 'InternalModule'], + ['externalNgModule', 'ExternalModule'], + ]); + }); + + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/methods.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + [ + 'function() { return { ngModule: InternalModule }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [SomeService] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: module.ExternalModule }; }', + 'ExternalModule', + ], + ]); + }); + + // https://github.com/angular/angular/issues/29078 + it('should resolve aliased module references to their original declaration', () => { + const {program, host: compilerHost} = makeTestBundleProgram(MODULE_WITH_PROVIDERS_PROGRAM); + const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); + const file = program.getSourceFile('/src/aliased_class.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.node.name.text])).toEqual([ + ['function() { return { ngModule: AliasedModule_1 }; }', 'AliasedModule'], + ]); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index f8c9023005..b92b7667d0 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -7,8 +7,9 @@ */ import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; -import {existsSync, readFileSync, readdirSync, statSync, writeFileSync} from 'fs'; +import {existsSync, readFileSync, readdirSync, statSync, symlinkSync} from 'fs'; import * as mockFs from 'mock-fs'; +import * as path from 'path'; import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers'; import {NodeJSFileSystem} from '../../src/file_system/node_js_file_system'; @@ -40,6 +41,7 @@ describe('ngcc main()', () => { describe('with targetEntryPointPath', () => { it('should only compile the given package entry-point (and its dependencies).', () => { const STANDARD_MARKERS = { + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', es2015: '0.0.0-PLACEHOLDER', esm5: '0.0.0-PLACEHOLDER', @@ -161,29 +163,31 @@ describe('ngcc main()', () => { }); - // * the `main` property is UMD, which is not yet supported. - // * none of the ES2015 formats are compiled as they are not on the `propertiesToConsider` - // list. + // The ES2015 formats are not compiled as they are not in `propertiesToConsider`. expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', }); expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({ esm5: '0.0.0-PLACEHOLDER', + main: '0.0.0-PLACEHOLDER', module: '0.0.0-PLACEHOLDER', fesm5: '0.0.0-PLACEHOLDER', typings: '0.0.0-PLACEHOLDER', @@ -195,12 +199,11 @@ describe('ngcc main()', () => { it('should only compile the first matching format', () => { mainNgcc({ basePath: '/node_modules', - propertiesToConsider: ['main', 'module', 'fesm5', 'esm5'], + propertiesToConsider: ['module', 'fesm5', 'esm5'], compileAllFormats: false, logger: new MockLogger(), }); - // * The `main` is UMD, which is not yet supported, and so is not compiled. // * In the Angular packages fesm5 and module have the same underlying format, // so both are marked as compiled. // * The `esm5` is not compiled because we stopped after the `fesm5` format. @@ -333,10 +336,15 @@ describe('ngcc main()', () => { function createMockFileSystem() { + const typeScriptPath = path.join(process.env.RUNFILES !, 'typescript'); + if (!existsSync(typeScriptPath)) { + symlinkSync(resolveNpmTreeArtifact('typescript'), typeScriptPath, 'junction'); + } + mockFs({ '/node_modules/@angular': loadAngularPackages(), - '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs', 'index.js')), - '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib', 'tslib.js')), + '/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs')), + '/node_modules/tslib': loadDirectory(resolveNpmTreeArtifact('tslib')), '/node_modules/test-package': { 'package.json': '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}', // no metadata.json file so not compiled by Angular. diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts index b976f9487a..4049ac9c25 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -5,10 +5,12 @@ * 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 {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; import {MockFileSystem} from '../helpers/mock_file_system'; +const _ = AbsoluteFsPath.from; + function createMockFileSystem() { return new MockFileSystem({ '/node_modules/test': { @@ -102,7 +104,7 @@ describe('entry point bundle', () => { // Modules resolved from "other" should be declaration files '/node_modules/other/public_api.d.ts', '/node_modules/other/index.d.ts', - ])); + ].map(p => _(p).toString()))); expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) .toEqual(jasmine.arrayWithExactContents([ @@ -115,6 +117,6 @@ describe('entry point bundle', () => { '/node_modules/test/secondary/index.d.ts', '/node_modules/other/public_api.d.ts', '/node_modules/other/index.d.ts', - ])); + ].map(p => _(p).toString()))); }); }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts index e93af39185..5f7dc3ae74 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_finder_spec.ts @@ -22,8 +22,8 @@ describe('findEntryPoints()', () => { let finder: EntryPointFinder; beforeEach(() => { const fs = createMockFileSystem(); - resolver = - new DependencyResolver(new MockLogger(), new EsmDependencyHost(fs, new ModuleResolver(fs))); + resolver = new DependencyResolver( + fs, new MockLogger(), {esm2015: new EsmDependencyHost(fs, new ModuleResolver(fs))}); spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => { return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []}; }); diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index 375fa013ac..c921fb38c2 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -12,7 +12,7 @@ import {getEntryPointInfo} from '../../src/packages/entry_point'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; -const _ = AbsoluteFsPath.fromUnchecked; +const _ = AbsoluteFsPath.from; describe('getEntryPointInfo()', () => { const SOME_PACKAGE = _('/some_package'); diff --git a/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts new file mode 100644 index 0000000000..6199050edb --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/commonjs_rendering_formatter_spec.ts @@ -0,0 +1,440 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; +import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; +import {CommonJsRenderingFormatter} from '../../src/rendering/commonjs_rendering_formatter'; +import {makeTestEntryPointBundle, getDeclaration, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {MockLogger} from '../helpers/mock_logger'; + +const _ = AbsoluteFsPath.fromUnchecked; + +function setup(file: {name: AbsoluteFsPath, contents: string}) { + const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); + const logger = new MockLogger(); + const bundle = makeTestEntryPointBundle('module', 'commonjs', false, [file]); + const typeChecker = bundle.src.program.getTypeChecker(); + const host = new CommonJsReflectionHost(logger, false, bundle.src.program, bundle.src.host); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = + new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); + const renderer = new CommonJsRenderingFormatter(host, false); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + return { + host, + program: bundle.src.program, + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager + }; +} + +const PROGRAM = { + name: _('/some/file.js'), + contents: ` +/* A copyright notice */ +require('some-side-effect'); +var core = require('@angular/core'); +var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] }, + { type: OtherA } + ]; + A.prototype.ngDoCheck = function() { + // + }; + return A; +}()); + +var B = (function() { + function B() {} + B.decorators = [ + { type: OtherB }, + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + +var C = (function() { + function C() {} + C.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; + return C; +}()); + +function NoIife() {} + +var BadIife = (function() { + function BadIife() {} + BadIife.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; +}()); + +var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; +var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; +function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} +// Some other content +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife;` +}; + +const PROGRAM_DECORATE_HELPER = { + name: _('/some/file.js'), + contents: ` +var tslib_1 = require("tslib"); +/* A copyright notice */ +var core = require('@angular/core'); +var OtherA = function () { return function (node) { }; }; +var OtherB = function () { return function (node) { }; }; +var A = /** @class */ (function () { + function A() { + } + A = tslib_1.__decorate([ + core.Directive({ selector: '[a]' }), + OtherA() + ], A); + return A; +}()); +exports.A = A; +var B = /** @class */ (function () { + function B() { + } + B = tslib_1.__decorate([ + OtherB(), + core.Directive({ selector: '[b]' }) + ], B); + return B; +}()); +exports.B = B; +var C = /** @class */ (function () { + function C() { + } + C = tslib_1.__decorate([ + core.Directive({ selector: '[c]' }) + ], C); + return C; +}()); +exports.C = C; +var D = /** @class */ (function () { + function D() { + } + D_1 = D; + var D_1; + D = D_1 = tslib_1.__decorate([ + core.Directive({ selector: '[d]', providers: [D_1] }) + ], D); + return D; +}()); +exports.D = D; +// Some other content` +}; + +describe('CommonJsRenderingFormatter', () => { + + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ +require('some-side-effect'); +var core = require('@angular/core'); +var i0 = require('@angular/core'); +var i1 = require('@angular/common');`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + expect(output.toString()).toContain(` +// Some other content +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +exports.ComponentA1 = i0.ComponentA1; +exports.ComponentA2 = i0.ComponentA2; +exports.ComponentB = i1.ComponentB; +exports.TopLevelComponent = TopLevelComponent;`); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` +var core = require('@angular/core'); + +var x = 3; +var A = (function() {`); + }); + + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); + expect(output.toString()).toContain(` +var core = require('@angular/core'); +var i0 = require('@angular/core'); + +var x = 3; +var A = (function() {`); + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + it('should insert the definitions directly before the return statement of the class IIFE', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` + A.prototype.ngDoCheck = function() { + // + }; +SOME DEFINITION TEXT + return A; +`); + }); + + it('should error if the compiledClass is not valid', () => { + const {renderer, sourceFile, program} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + + const noIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: _('NoIife')}; + expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); + + const badIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: _('BadIife')}; + expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); + }); + }); + + + describe('removeDecorators', () => { + + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); + expect(output.toString()).not.toContain(`C.decorators`); + }); + + }); + + describe('[__decorate declarations]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts new file mode 100644 index 0000000000..5f27b1a847 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts @@ -0,0 +1,181 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {fromObject} from 'convert-source-map'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {ModuleWithProvidersAnalyzer, ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {DtsRenderer} from '../../src/rendering/dts_renderer'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; + +const _ = AbsoluteFsPath.fromUnchecked; + +class TestRenderingFormatter implements RenderingFormatter { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { + output.prepend('\n// ADD IMPORTS\n'); + } + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { + output.prepend('\n// ADD EXPORTS\n'); + } + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + output.prepend('\n// ADD CONSTANTS\n'); + } + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) { + output.prepend('\n// ADD DEFINITIONS\n'); + } + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) { + output.prepend('\n// REMOVE DECORATORS\n'); + } + rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { + output.prepend('\n// REWRITTEN DECLARATIONS\n'); + } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } +} + +function createTestRenderer( + packageName: string, files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string}[], + mappingFiles?: {name: string, contents: string}[]) { + const logger = new MockLogger(); + const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); + const isCore = packageName === '@angular/core'; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); + const typeChecker = bundle.src.program.getTypeChecker(); + const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) + .analyzeProgram(); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const privateDeclarationsAnalyses = + new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); + + const renderer = new DtsRenderer(testFormatter, fs, logger, host, isCore, bundle); + + return {renderer, + testFormatter, + decorationAnalyses, + moduleWithProvidersAnalyses, + privateDeclarationsAnalyses, + bundle}; +} + + +describe('DtsRenderer', () => { + const INPUT_PROGRAM = { + name: '/src/file.js', + contents: + `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` + }; + const INPUT_DTS_PROGRAM = { + name: '/typings/file.d.ts', + contents: `export declare class A {\nfoo(x: number): number;\n}\n` + }; + + const INPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'file': '/src/file.js', + 'sourceRoot': '', + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': + 'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; + + const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': ';;;;;;;;;;AAAA', + 'file': 'file.js', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + it('should render extract types into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents) + .toContain( + 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta'); + }); + + it('should render imports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); + }); + + it('should render exports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + // Add a mock export to trigger export rendering + privateDeclarationsAnalyses.push( + {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); + }); + + it('should render ModuleWithProviders type params', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD MODUlE WITH PROVIDERS PARAMS\n`); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts similarity index 88% rename from packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts index 83c8b253cf..4f8e69e5ca 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts @@ -7,12 +7,15 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; -import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; +import {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; @@ -32,11 +35,12 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new Esm5Renderer(fs, logger, host, false, bundle); + const renderer = new Esm5RenderingFormatter(host, false); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager }; } @@ -87,8 +91,8 @@ var BadIife = (function() { var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { - const compilerFactory = injector.get(CompilerFactory); - const compiler = compilerFactory.createCompiler([options]); + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); return compiler.compileModuleAsync(moduleType); } @@ -151,7 +155,7 @@ export { D }; // Some other content` }; -describe('Esm5Renderer', () => { +describe('Esm5RenderingFormatter', () => { describe('addImports', () => { it('should insert the given imports after existing imports of the source file', () => { @@ -174,14 +178,17 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); expect(output.toString()).toContain(` export {A, B, C, NoIife, BadIife}; export {ComponentA1} from './a'; @@ -191,14 +198,17 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); expect(outputString).not.toContain(`{eComponentB as ComponentB}`); @@ -214,11 +224,11 @@ export {TopLevelComponent};`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); @@ -229,13 +239,13 @@ var A = (function() {`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; import * as i0 from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); }); @@ -281,7 +291,7 @@ SOME DEFINITION TEXT }); it('should error if the compiledClass is not valid', () => { - const {renderer, host, sourceFile, program} = setup(PROGRAM); + const {renderer, sourceFile, program} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); const noIifeDeclaration = @@ -355,9 +365,7 @@ SOME DEFINITION TEXT expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); expect(output.toString()).toContain(`{ type: OtherB }`); expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`); - expect(output.toString()).not.toContain(`C.decorators = [ - { type: Directive, args: [{ selector: '[c]' }] }, -];`); + expect(output.toString()).not.toContain(`C.decorators`); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts similarity index 59% rename from packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts index 34ba2583bf..fc68fee711 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts @@ -7,35 +7,43 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {EsmRenderer} from '../../src/rendering/esm_renderer'; +import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter'; import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; +import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; const _ = AbsoluteFsPath.fromUnchecked; -function setup(file: {name: AbsoluteFsPath, contents: string}) { +function setup( + files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]) { const fs = new MockFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, files, dtsFiles) !; const typeChecker = bundle.src.program.getTypeChecker(); - const host = new Esm2015ReflectionHost(logger, false, typeChecker); + const host = new Esm2015ReflectionHost(logger, false, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); const decorationAnalyses = new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, [_('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new EsmRenderer(fs, logger, host, false, bundle); + const renderer = new EsmRenderingFormatter(host, false); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, + bundle, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, }; } @@ -75,9 +83,207 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { // Some other content` }; -const PROGRAM_DECORATE_HELPER = { - name: _('/some/file.js'), - contents: ` +describe('EsmRenderingFormatter', () => { + + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ +import 'some-side-effect'; +import {Directive} from '@angular/core'; +import * as i0 from '@angular/core'; +import * as i1 from '@angular/common';`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + expect(output.toString()).toContain(` +// Some other content +export {ComponentA1} from './a'; +export {ComponentA2} from './a'; +export {ComponentB} from './foo/b'; +export {TopLevelComponent};`); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + expect(output.toString()).toContain(` +import {Directive} from '@angular/core'; + +const x = 3; +export class A {}`); + }); + + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); + expect(output.toString()).toContain(` +import {Directive} from '@angular/core'; +import * as i0 from '@angular/core'; + +const x = 3; +export class A {`); + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + it('should insert the definitions directly after the class declaration', () => { + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` +export class A {} +SOME DEFINITION TEXT +A.decorators = [ +`); + }); + + }); + + + describe('removeDecorators', () => { + describe('[static property declaration]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).not.toContain(`C.decorators = [`); + }); + }); + }); + + describe('[__decorate declarations]', () => { + + const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` import * as tslib_1 from "tslib"; var D_1; /* A copyright notice */ @@ -111,201 +317,10 @@ D = D_1 = tslib_1.__decorate([ ], D); export { D }; // Some other content` -}; + }; -describe('Esm2015Renderer', () => { - - describe('addImports', () => { - it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addImports( - output, - [ - {specifier: '@angular/core', qualifier: 'i0'}, - {specifier: '@angular/common', qualifier: 'i1'} - ], - sourceFile); - expect(output.toString()).toContain(`/* A copyright notice */ -import 'some-side-effect'; -import {Directive} from '@angular/core'; -import * as i0 from '@angular/core'; -import * as i1 from '@angular/common';`); - }); - }); - - describe('addExports', () => { - it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); - expect(output.toString()).toContain(` -// Some other content -export {ComponentA1} from './a'; -export {ComponentA2} from './a'; -export {ComponentB} from './foo/b'; -export {TopLevelComponent};`); - }); - - it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); - const outputString = output.toString(); - expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); - expect(outputString).not.toContain(`{eComponentB as ComponentB}`); - expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); - }); - }); - - describe('addConstants', () => { - it('should insert the given constants after imports in the source file', () => { - const {renderer, program} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); - expect(output.toString()).toContain(` -import {Directive} from '@angular/core'; - -const x = 3; -export class A {}`); - }); - - it('should insert constants after inserted imports', () => { - const {renderer, program} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); - renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); - expect(output.toString()).toContain(` -import {Directive} from '@angular/core'; -import * as i0 from '@angular/core'; - -const x = 3; -export class A {`); - }); - }); - - describe('rewriteSwitchableDeclarations', () => { - it('should switch marked declaration initializers', () => { - const {renderer, program, switchMarkerAnalyses, sourceFile} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.rewriteSwitchableDeclarations( - output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); - expect(output.toString()) - .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); - expect(output.toString()) - .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); - expect(output.toString()) - .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); - expect(output.toString()) - .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); - expect(output.toString()) - .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); - }); - }); - - describe('addDefinitions', () => { - it('should insert the definitions directly after the class declaration', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; - renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); - expect(output.toString()).toContain(` -export class A {} -SOME DEFINITION TEXT -A.decorators = [ -`); - }); - - }); - - - describe('removeDecorators', () => { - describe('[static property declaration]', () => { - it('should delete the decorator (and following comma) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); - - - it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); - - - it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - expect(output.toString()).not.toContain(`C.decorators = [`); - }); - }); - }); - - describe('[__decorate declarations]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; @@ -322,7 +337,7 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; @@ -340,7 +355,7 @@ A.decorators = [ it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; @@ -357,4 +372,140 @@ A.decorators = [ expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`); }); }); + + describe('addModuleWithProvidersParams', () => { + const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/index.js', + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class SomeClass {} + export class SomeModule { + static withProviders1() { return {ngModule: SomeModule}; } + static withProviders2() { return {ngModule: SomeModule}; } + static withProviders3() { return {ngModule: SomeClass}; } + static withProviders4() { return {ngModule: ExternalModule}; } + static withProviders5() { return {ngModule: ExternalModule}; } + static withProviders6() { return {ngModule: LibraryModule}; } + static withProviders7() { return {ngModule: SomeModule, providers: []}; }; + static withProviders8() { return {ngModule: SomeModule}; } + } + export function withProviders1() { return {ngModule: SomeModule}; } + export function withProviders2() { return {ngModule: SomeModule}; } + export function withProviders3() { return {ngModule: SomeClass}; } + export function withProviders4() { return {ngModule: ExternalModule}; } + export function withProviders5() { return {ngModule: ExternalModule}; } + export function withProviders6() { return {ngModule: LibraryModule}; } + export function withProviders7() { return {ngModule: SomeModule, providers: []}; }; + export function withProviders8() { return {ngModule: SomeModule}; }`, + }, + { + name: '/src/module.js', + contents: ` + export class ExternalModule { + static withProviders1() { return {ngModule: ExternalModule}; } + static withProviders2() { return {ngModule: ExternalModule}; } + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ + { + name: '/typings/index.d.ts', + contents: ` + import {ModuleWithProviders} from '@angular/core'; + export declare class SomeClass {} + export interface MyModuleWithProviders extends ModuleWithProviders {} + export declare class SomeModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(); + static withProviders6(): ModuleWithProviders; + static withProviders7(): {ngModule: SomeModule, providers: any[]}; + static withProviders8(): MyModuleWithProviders; + } + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(); + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; + export declare function withProviders8(): MyModuleWithProviders;` + }, + { + name: '/typings/module.d.ts', + contents: ` + export interface ModuleWithProviders {} + export declare class ExternalModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + + it('should fixup functions/methods that return ModuleWithProviders structures', () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/index.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + + expect(output.toString()).toContain(` + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(): i1.ModuleWithProviders; + static withProviders6(): ModuleWithProviders; + static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + expect(output.toString()).toContain(` + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(): i1.ModuleWithProviders; + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + }); + + it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core', + () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/module.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + expect(output.toString()).toContain(` + static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; + static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); + }); + }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index f47ac9b248..a20e13174b 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -9,36 +9,26 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; -import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; -import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; +import {ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; -import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; -import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; -import {Logger} from '../../src/logging/logger'; -import {MockFileSystem} from '../helpers/mock_file_system'; -import {MockLogger} from '../helpers/mock_logger'; -import {FileSystem} from '../../src/file_system/file_system'; - const _ = AbsoluteFsPath.fromUnchecked; -class TestRenderer extends Renderer { - constructor( - fs: FileSystem, logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } - addImports( - output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile) { +import {Renderer} from '../../src/rendering/renderer'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +class TestRenderingFormatter implements RenderingFormatter { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } - addExports(output: MagicString, baseEntryPointPath: string, exports: { - identifier: string, - from: string - }[]) { + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { output.prepend('\n// ADD EXPORTS\n'); } addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { @@ -53,6 +43,11 @@ class TestRenderer extends Renderer { rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { output.prepend('\n// REWRITTEN DECLARATIONS\n'); } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } } function createTestRenderer( @@ -71,17 +66,25 @@ function createTestRenderer( typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const moduleWithProvidersAnalyses = - new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); - const renderer = new TestRenderer(fs, logger, host, isCore, bundle); - spyOn(renderer, 'addImports').and.callThrough(); - spyOn(renderer, 'addDefinitions').and.callThrough(); - spyOn(renderer, 'removeDecorators').and.callThrough(); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); - return {renderer, decorationAnalyses, switchMarkerAnalyses, moduleWithProvidersAnalyses, - privateDeclarationsAnalyses}; + const renderer = new Renderer(testFormatter, fs, logger, host, isCore, bundle); + + return {renderer, + testFormatter, + decorationAnalyses, + switchMarkerAnalyses, + privateDeclarationsAnalyses, + bundle}; } @@ -91,10 +94,6 @@ describe('Renderer', () => { contents: `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` }; - const INPUT_DTS_PROGRAM = { - name: '/typings/file.d.ts', - contents: `export declare class A {\nfoo(x: number): number;\n}\n` - }; const COMPONENT_PROGRAM = { name: '/src/component.js', @@ -113,9 +112,17 @@ describe('Renderer', () => { 'sourcesContent': [INPUT_PROGRAM.contents] }); - const RENDERED_CONTENTS = - `\n// ADD EXPORTS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n\n// REMOVE DECORATORS\n` + - INPUT_PROGRAM.contents; + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; const OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, @@ -138,11 +145,10 @@ describe('Renderer', () => { describe('renderProgram()', () => { it('should render the modified contents; and a new map file, if the original provided no map file.', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -153,18 +159,16 @@ describe('Renderer', () => { it('should render as JavaScript', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); + renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toEqual( `A.ngComponentDef = ɵngcc0.ɵɵdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) { ɵngcc0.ɵɵtext(0); } if (rf & 2) { ɵngcc0.ɵɵselect(0); - ɵngcc0.ɵɵtextBinding(0, ɵngcc0.ɵɵinterpolation1("", ctx.person.name, "")); + ɵngcc0.ɵɵtextInterpolate(ctx.person.name); } }, encapsulation: 2 }); /*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{ type: Component, @@ -173,16 +177,14 @@ describe('Renderer', () => { }); - describe('calling abstract methods', () => { + describe('calling RenderingFormatter methods', () => { it('should call addImports with the source code and info about the core Angular library.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addImportsSpy = renderer.addImports as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: '@angular/core', qualifier: 'ɵngcc0'} @@ -192,12 +194,10 @@ describe('Renderer', () => { it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ name: _('A'), @@ -215,12 +215,10 @@ describe('Renderer', () => { it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy; expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); // Each map key is the TS node of the decorator container @@ -236,21 +234,34 @@ describe('Renderer', () => { expect(values[0][0].getText()) .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); }); + + it('should call renderImports after other abstract methods', () => { + // This allows the other methods to add additional imports if necessary + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const addExportsSpy = testFormatter.addExports as jasmine.Spy; + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + const addConstantsSpy = testFormatter.addConstants as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); + }); }); describe('source map merging', () => { it('should merge any inline source map from the original file and write the output as an inline source map', () => { - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer( 'test-package', [{ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() }]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); @@ -265,12 +276,10 @@ describe('Renderer', () => { }]; const mappingFiles = [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -293,15 +302,13 @@ describe('Renderer', () => { }; // The package name of `@angular/core` indicates that we are compiling the core library. const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: './r3_symbols', qualifier: 'ɵngcc0'} ]); @@ -315,227 +322,15 @@ describe('Renderer', () => { }; const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([]); }); }); - - describe('rendering typings', () => { - it('should render extract types into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain( - 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵɵDirectiveDefWithMeta'); - }); - - it('should render imports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`); - }); - - it('should render exports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - - // Add a mock export to trigger export rendering - privateDeclarationsAnalyses.push( - {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`); - }); - - it('should fixup functions/methods that return ModuleWithProviders structures', () => { - const MODULE_WITH_PROVIDERS_PROGRAM = [ - { - name: '/src/index.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class SomeClass {} - export class SomeModule { - static withProviders1() { - return {ngModule: SomeModule}; - } - static withProviders2() { - return {ngModule: SomeModule}; - } - static withProviders3() { - return {ngModule: SomeClass}; - } - static withProviders4() { - return {ngModule: ExternalModule}; - } - static withProviders5() { - return {ngModule: ExternalModule}; - } - static withProviders6() { - return {ngModule: LibraryModule}; - } - static withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - static withProviders8() { - return {ngModule: SomeModule}; - } - } - export function withProviders1() { - return {ngModule: SomeModule}; - } - export function withProviders2() { - return {ngModule: SomeModule}; - } - export function withProviders3() { - return {ngModule: SomeClass}; - } - export function withProviders4() { - return {ngModule: ExternalModule}; - } - export function withProviders5() { - return {ngModule: ExternalModule}; - } - export function withProviders6() { - return {ngModule: LibraryModule}; - } - export function withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - export function withProviders8() { - return {ngModule: SomeModule}; - }`, - }, - { - name: '/src/module.js', - contents: ` - export class ExternalModule { - static withProviders1() { - return {ngModule: ExternalModule}; - } - static withProviders2() { - return {ngModule: ExternalModule}; - } - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ - { - name: '/typings/index.d.ts', - contents: ` - import {ModuleWithProviders} from '@angular/core'; - export declare class SomeClass {} - export interface MyModuleWithProviders extends ModuleWithProviders {} - export declare class SomeModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders; - static withProviders5(); - static withProviders6(): ModuleWithProviders; - static withProviders7(): {ngModule: SomeModule, providers: any[]}; - static withProviders8(): MyModuleWithProviders; - } - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders; - export declare function withProviders5(); - export declare function withProviders6(): ModuleWithProviders; - export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; - export declare function withProviders8(): MyModuleWithProviders;` - }, - { - name: '/typings/module.d.ts', - contents: ` - export interface ModuleWithProviders {} - export declare class ExternalModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer( - 'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !; - - expect(typingsFile.contents).toContain(` - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - expect(typingsFile.contents).toContain(` - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - - expect(renderer.addImports) - .toHaveBeenCalledWith( - jasmine.any(MagicString), - [ - {specifier: './module', qualifier: 'ɵngcc0'}, - {specifier: '@angular/core', qualifier: 'ɵngcc1'}, - {specifier: 'some-library', qualifier: 'ɵngcc2'}, - ], - jasmine.anything()); - - - // The following expectation checks that we do not mistake `ModuleWithProviders` types - // that are not imported from `@angular/core`. - const typingsFile2 = result.find(f => f.path === '/typings/module.d.ts') !; - expect(typingsFile2.contents).toContain(` - static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; - static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); - }); - }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts new file mode 100644 index 0000000000..8d6e09d6b1 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts @@ -0,0 +1,490 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {UmdReflectionHost} from '../../src/host/umd_host'; +import {ImportManager} from '../../../src/ngtsc/translator'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; + +const _ = AbsoluteFsPath.fromUnchecked; + +function setup(file: {name: string, contents: string}) { + const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); + const logger = new MockLogger(); + const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file]); + const src = bundle.src; + const typeChecker = src.program.getTypeChecker(); + const host = new UmdReflectionHost(logger, false, src.program, src.host); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, src.program, src.options, src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); + const renderer = new UmdRenderingFormatter(host, false); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + return { + decorationAnalyses, + host, + importManager, + program: src.program, renderer, + sourceFile: src.file, switchMarkerAnalyses + }; +} + +const PROGRAM = { + name: _('/some/file.js'), + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core'], factory) : +(factory(global.file,global.someSideEffect,global.localDep,global.ng.core)); +}(this, (function (exports,someSideEffect,localDep,core) {'use strict'; +var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] }, + { type: OtherA } + ]; + A.prototype.ngDoCheck = function() { + // + }; + return A; +}()); + +var B = (function() { + function B() {} + B.decorators = [ + { type: OtherB }, + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + +var C = (function() { + function C() {} + C.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; + return C; +}()); + +function NoIife() {} + +var BadIife = (function() { + function BadIife() {} + BadIife.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; +}()); + +var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; +var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; +function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} +// Some other content +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +})));`, +}; + + +const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('tslib'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','/tslib','@angular/core'], factory) : +(factory(global.file,global.tslib,global.ng.core)); +}(this, (function (exports,tslib,core) {'use strict'; + var OtherA = function () { return function (node) { }; }; + var OtherB = function () { return function (node) { }; }; + var A = /** @class */ (function () { + function A() { + } + A = tslib.__decorate([ + core.Directive({ selector: '[a]' }), + OtherA() + ], A); + return A; + }()); + export { A }; + var B = /** @class */ (function () { + function B() { + } + B = tslib.__decorate([ + OtherB(), + core.Directive({ selector: '[b]' }) + ], B); + return B; + }()); + export { B }; + var C = /** @class */ (function () { + function C() { + } + C = tslib.__decorate([ + core.Directive({ selector: '[c]' }) + ], C); + return C; + }()); + export { C }; + var D = /** @class */ (function () { + function D() { + } + D_1 = D; + var D_1; + D = D_1 = tslib.__decorate([ + core.Directive({ selector: '[d]', providers: [D_1] }) + ], D); + return D; + }()); + exports.D = D; + // Some other content +})));` +}; + +describe('UmdRenderingFormatter', () => { + + describe('addImports', () => { + it('should append the given imports into the CommonJS factory call', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core'),require('@angular/core'),require('@angular/common')) :`); + }); + + it('should append the given imports into the AMD initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core','@angular/core','@angular/common'], factory) :`); + }); + + it('should append the given imports into the global initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `(factory(global.file,global.someSideEffect,global.localDep,global.ng.core,global.ng.core,global.ng.common));`); + }); + + it('should append the given imports as parameters into the factory function definition', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain(`(function (exports,someSideEffect,localDep,core,i0,i1) {'use strict';`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const generateNamedImportSpy = spyOn(importManager, 'generateNamedImport').and.callThrough(); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), identifier: 'ComponentB'}, + {from: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + + expect(output.toString()).toContain(` +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +exports.ComponentA1 = i0.ComponentA1; +exports.ComponentA2 = i0.ComponentA2; +exports.ComponentB = i1.ComponentB; +exports.TopLevelComponent = TopLevelComponent; +})));`); + + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./foo/b', 'ComponentB'); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`eComponentA1`); + expect(outputString).not.toContain(`eComponentB`); + expect(outputString).not.toContain(`eTopLevelComponent`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` +}(this, (function (exports,someSideEffect,localDep,core) { +var x = 3; +'use strict'; +var A = (function() {`); + }); + + it('should insert constants after inserted imports', + () => { + // This test (from ESM5) is not needed as constants go in the body + // of the UMD IIFE, so cannot come before imports. + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + it('should insert the definitions directly before the return statement of the class IIFE', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` + A.prototype.ngDoCheck = function() { + // + }; +SOME DEFINITION TEXT + return A; +`); + }); + + it('should error if the compiledClass is not valid', () => { + const {renderer, sourceFile, program} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + + const noIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; + expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); + + const badIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; + expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); + }); + }); + + describe('removeDecorators', () => { + + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).not.toContain(`C.decorators`); + }); + + }); + + describe('[__decorate declarations]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts index f3c13a01db..7e4ba0619a 100644 --- a/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/in_place_file_writer_spec.ts @@ -11,7 +11,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {InPlaceFileWriter} from '../../src/writing/in_place_file_writer'; import {MockFileSystem} from '../helpers/mock_file_system'; -const _ = AbsoluteFsPath.fromUnchecked; +const _ = AbsoluteFsPath.from; function createMockFileSystem() { return new MockFileSystem({ @@ -71,13 +71,14 @@ describe('InPlaceFileWriter', () => { it('should error if the backup file already exists', () => { const fs = createMockFileSystem(); const fileWriter = new InPlaceFileWriter(fs); + const absoluteBackupPath = _('/package/path/already-backed-up.js'); expect( () => fileWriter.writeBundle( {} as EntryPoint, {} as EntryPointBundle, [ - {path: _('/package/path/already-backed-up.js'), contents: 'MODIFIED BACKED UP'}, + {path: absoluteBackupPath, contents: 'MODIFIED BACKED UP'}, ])) .toThrowError( - 'Tried to overwrite /package/path/already-backed-up.js.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.'); + `Tried to overwrite ${absoluteBackupPath}.__ivy_ngcc_bak with an ngcc back up file, which is disallowed.`); }); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index f65b93db86..fcb414c366 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler'; +import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler'; +import {STRING_TYPE} from '@angular/compiler/src/output/output_ast'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -18,7 +19,6 @@ import {NgModuleRouteAnalyzer} from '../../routing'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {getSourceFile} from '../../util/src/typescript'; - import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; import {combineResolvers, findAngularDecorator, forwardRefResolver, getValidConstructorDependencies, isExpressionForwardReference, toR3Reference, unwrapExpression} from './util'; @@ -43,7 +43,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler|null>(); constructor( - private program: ts.Program, private checker: ts.TypeChecker, - private options: ts.CompilerOptions, private host: ts.CompilerHost) {} + protected program: ts.Program, protected checker: ts.TypeChecker, + protected options: ts.CompilerOptions, protected host: ts.CompilerHost, + private reflectionHost: ReflectionHost) {} emit(ref: Reference, context: ts.SourceFile, importMode: ImportMode): Expression|null { if (ref.bestGuessOwningModule === null) { @@ -159,7 +162,7 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { return this.moduleExportsCache.get(moduleName) !; } - private enumerateExportsOfModule(specifier: string, fromFile: string): + protected enumerateExportsOfModule(specifier: string, fromFile: string): Map|null { // First, resolve the module specifier to its entry point, and get the ts.Symbol for it. const resolvedModule = resolveModuleName(specifier, fromFile, this.options, this.host); @@ -172,34 +175,12 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { return null; } - const entryPointSymbol = this.checker.getSymbolAtLocation(entryPointFile); - if (entryPointSymbol === undefined) { + const exports = this.reflectionHost.getExportsOfModule(entryPointFile); + if (exports === null) { return null; } - - // Next, build a Map of all the ts.Declarations exported via the specifier and their exported - // names. const exportMap = new Map(); - - const exports = this.checker.getExportsOfModule(entryPointSymbol); - for (const expSymbol of exports) { - // Resolve export symbols to their actual declarations. - const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ? - this.checker.getAliasedSymbol(expSymbol) : - expSymbol; - - // At this point the valueDeclaration of the symbol should be defined. - const decl = declSymbol.valueDeclaration; - if (decl === undefined) { - continue; - } - - // Prefer importing the symbol via its declared name, but take any export of it otherwise. - if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) { - exportMap.set(decl, expSymbol.name); - } - } - + exports.forEach((declaration, name) => { exportMap.set(declaration.node, name); }); return exportMap; } } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index 66c9f9f737..c8770b758d 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -31,7 +31,7 @@ export interface DirectiveMeta extends T2DirectiveMeta { */ selector: string; queries: string[]; - ngTemplateGuards: string[]; + ngTemplateGuards: TemplateGuardMeta[]; hasNgTemplateContextGuard: boolean; /** @@ -43,6 +43,25 @@ export interface DirectiveMeta extends T2DirectiveMeta { baseClass: Reference|'dynamic'|null; } +/** + * Metadata that describes a template guard for one of the directive's inputs. + */ +export interface TemplateGuardMeta { + /** + * The input name that this guard should be applied to. + */ + inputName: string; + + /** + * Represents the type of the template guard. + * + * - 'invocation' means that a call to the template guard function is emitted so that its return + * type can result in narrowing of the input type. + * - 'binding' means that the input binding expression itself is used as template guard. + */ + type: 'invocation'|'binding'; +} + /** * Metadata for a pipe within an NgModule's scope. */ diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts index fbb67aac59..31a39e9362 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts @@ -9,10 +9,10 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; -import {ClassDeclaration, ClassMemberKind, ReflectionHost, isNamedClassDeclaration, reflectTypeEntityToDeclaration} from '../../reflection'; +import {ClassDeclaration, ClassMember, ClassMemberKind, ReflectionHost, isNamedClassDeclaration, reflectTypeEntityToDeclaration} from '../../reflection'; import {nodeDebugInfo} from '../../util/src/typescript'; -import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api'; +import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api'; export function extractReferencesFromType( checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string | null, @@ -80,20 +80,39 @@ export function readStringArrayType(type: ts.TypeNode): string[] { export function extractDirectiveGuards(node: ClassDeclaration, reflector: ReflectionHost): { - ngTemplateGuards: string[], + ngTemplateGuards: TemplateGuardMeta[], hasNgTemplateContextGuard: boolean, } { - const methods = nodeStaticMethodNames(node, reflector); - const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_')) - .map(method => method.split('_', 2)[1]); - const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard'); + const staticMembers = reflector.getMembersOfClass(node).filter(member => member.isStatic); + const ngTemplateGuards = staticMembers.map(extractTemplateGuard) + .filter((guard): guard is TemplateGuardMeta => guard !== null); + const hasNgTemplateContextGuard = staticMembers.some( + member => member.kind === ClassMemberKind.Method && member.name === 'ngTemplateContextGuard'); return {hasNgTemplateContextGuard, ngTemplateGuards}; } -function nodeStaticMethodNames(node: ClassDeclaration, reflector: ReflectionHost): string[] { - return reflector.getMembersOfClass(node) - .filter(member => member.kind === ClassMemberKind.Method && member.isStatic) - .map(member => member.name); +function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null { + if (!member.name.startsWith('ngTemplateGuard_')) { + return null; + } + const inputName = member.name.split('_', 2)[1]; + if (member.kind === ClassMemberKind.Property) { + let type: string|null = null; + if (member.type !== null && ts.isLiteralTypeNode(member.type) && + ts.isStringLiteral(member.type.literal)) { + type = member.type.literal.text; + } + + // Only property members with string literal type 'binding' are considered as template guard. + if (type !== 'binding') { + return null; + } + return {inputName, type}; + } else if (member.kind === ClassMemberKind.Method) { + return {inputName, type: 'invocation'}; + } else { + return null; + } } /** diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts index a0553d2b78..85880d8e74 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/dynamic.ts @@ -32,7 +32,8 @@ export const enum DynamicValueReason { /** * An external reference could not be resolved to a value which can be evaluated. - * (E.g. a call expression for a function declared in `.d.ts`.) + * For example a call expression for a function declared in `.d.ts`, or accessing native globals + * such as `window`. */ EXTERNAL_REFERENCE, diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index 80005e48a0..56b9ac70c0 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; import {OwningModule} from '../../imports/src/references'; import {Declaration, ReflectionHost} from '../../reflection'; +import {isDeclaration} from '../../util/src/typescript'; import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; @@ -361,12 +362,15 @@ export class StaticInterpreter { } } return value; + } else if (isDeclaration(ref)) { + return DynamicValue.fromDynamicInput( + node, DynamicValue.fromExternalReference(ref, lhs as Reference)); } } else if (lhs instanceof DynamicValue) { return DynamicValue.fromDynamicInput(node, lhs); - } else { - throw new Error(`Invalid dot property access: ${lhs} dot ${rhs}`); } + + return DynamicValue.fromUnknown(node); } private visitCallExpression(node: ts.CallExpression, context: Context): ResolvedValue { diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index a4a1d5523a..4cf93ea800 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -122,6 +122,20 @@ describe('ngtsc metadata', () => { expect(evaluate(`const x = 3;`, '!!x')).toEqual(true); }); + it('resolves access from external variable declarations as dynamic value', () => { + const value = evaluate('declare const window: any;', 'window.location'); + if (!(value instanceof DynamicValue)) { + return fail(`Should have resolved to a DynamicValue`); + } + expect(value.isFromDynamicInput()).toEqual(true); + expect(value.node.getText()).toEqual('window.location'); + if (!(value.reason instanceof DynamicValue)) { + return fail(`Should have a DynamicValue as reason`); + } + expect(value.reason.isFromExternalReference()).toEqual(true); + expect(value.reason.node.getText()).toEqual('window: any'); + }); + it('imports work', () => { const {program} = makeProgram([ {name: 'second.ts', contents: 'export function foo(bar) { return bar; }'}, diff --git a/packages/compiler-cli/src/ngtsc/path/src/logical.ts b/packages/compiler-cli/src/ngtsc/path/src/logical.ts index e00a74f4ed..4ad75a3f59 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/logical.ts +++ b/packages/compiler-cli/src/ngtsc/path/src/logical.ts @@ -67,7 +67,7 @@ export class LogicalFileSystem { * `logicalPathOfFile(AbsoluteFsPath.fromSourceFile(sf))`. */ logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null { - return this.logicalPathOfFile(sf.fileName as AbsoluteFsPath); + return this.logicalPathOfFile(AbsoluteFsPath.from(sf.fileName)); } /** diff --git a/packages/compiler-cli/src/ngtsc/path/src/types.ts b/packages/compiler-cli/src/ngtsc/path/src/types.ts index 6561d7e2fe..7286aeaaa7 100644 --- a/packages/compiler-cli/src/ngtsc/path/src/types.ts +++ b/packages/compiler-cli/src/ngtsc/path/src/types.ts @@ -40,6 +40,12 @@ export const AbsoluteFsPath = { * Convert the path `str` to an `AbsoluteFsPath`, throwing an error if it's not an absolute path. */ from: function(str: string): AbsoluteFsPath { + if (str.startsWith('/') && process.platform === 'win32') { + // in Windows if it's absolute path and starts with `/` we shall + // resolve it and return it including the drive. + str = path.resolve(str); + } + const normalized = normalizeSeparators(str); if (!isAbsolutePath(normalized)) { throw new Error(`Internal Error: AbsoluteFsPath.from(${str}): path is not absolute`); @@ -80,6 +86,9 @@ export const AbsoluteFsPath = { */ resolve: function(basePath: string, ...paths: string[]): AbsoluteFsPath { return AbsoluteFsPath.from(path.resolve(basePath, ...paths));}, + + /** Returns true when the path provided is the root path. */ + isRoot: function(path: AbsoluteFsPath): boolean { return AbsoluteFsPath.dirname(path) === path;}, }; /** diff --git a/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts b/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts index 41f7582987..18910ceaca 100644 --- a/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts +++ b/packages/compiler-cli/src/ngtsc/path/test/types_spec.ts @@ -11,7 +11,7 @@ import {AbsoluteFsPath} from '../src/types'; describe('path types', () => { describe('AbsoluteFsPath', () => { it('should not throw when creating one from an absolute path', - () => { expect(AbsoluteFsPath.from('/test.txt')).toEqual('/test.txt'); }); + () => { expect(() => AbsoluteFsPath.from('/test.txt')).not.toThrow(); }); it('should not throw when creating one from a windows absolute path', () => { expect(AbsoluteFsPath.from('C:\\test.txt')).toEqual('C:/test.txt'); }); it('should not throw when creating one from a windows absolute path with POSIX separators', diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index c3ab8ca2de..0d768d3df6 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -438,7 +438,8 @@ export class NgtscProgram implements api.Program { // First, try to use local identifiers if available. new LocalIdentifierStrategy(), // Next, attempt to use an absolute import. - new AbsoluteModuleStrategy(this.tsProgram, checker, this.options, this.host), + new AbsoluteModuleStrategy( + this.tsProgram, checker, this.options, this.host, this.reflector), // Finally, check if the reference is being written into a file within the project's logical // file system, and use a relative import if so. If this fails, ReferenceEmitter will throw // an error. @@ -500,7 +501,8 @@ export class NgtscProgram implements api.Program { this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore, - this.routeAnalyzer, this.refEmitter, this.defaultImportTracker), + this.routeAnalyzer, this.refEmitter, this.defaultImportTracker, + this.options.i18nInLocale), new PipeDecoratorHandler( this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), ]; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts index 329156266c..e1ed7126ad 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts @@ -73,7 +73,8 @@ export class FactoryGenerator implements ShimGenerator { // This will encompass a lot of symbols which don't need factories, but that's okay // because it won't miss any that do. const varLines = symbolNames.map( - name => `export const ${name}NgFactory = new i0.ɵNgModuleFactory(${name});`); + name => + `export const ${name}NgFactory: i0.ɵNgModuleFactory = new i0.ɵNgModuleFactory(${name});`); sourceText += [ // This might be incorrect if the current package being compiled is Angular core, but it's // okay to leave in at type checking time. TypeScript can handle this reference via its path diff --git a/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel index 381f9cdd88..de435dc818 100644 --- a/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/shims/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler-cli/src/ngtsc/shims", + "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts index 51bb2be60d..5ceffe4118 100644 --- a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts +++ b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts @@ -117,30 +117,7 @@ export function getDeclaration( if (!sf) { throw new Error(`No such file: ${fileName}`); } - - let chosenDecl: ts.Declaration|null = null; - - sf.statements.forEach(stmt => { - if (chosenDecl !== null) { - return; - } else if (ts.isVariableStatement(stmt)) { - stmt.declarationList.declarations.forEach(decl => { - if (bindingNameEquals(decl.name, name)) { - chosenDecl = decl; - } - }); - } else if (ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) { - if (stmt.name !== undefined && stmt.name.text === name) { - chosenDecl = stmt; - } - } else if ( - ts.isImportDeclaration(stmt) && stmt.importClause !== undefined && - stmt.importClause.name !== undefined && stmt.importClause.name.text === name) { - chosenDecl = stmt.importClause; - } - }); - - chosenDecl = chosenDecl as ts.Declaration | null; + const chosenDecl = walkForDeclaration(sf); if (chosenDecl === null) { throw new Error(`No such symbol: ${name} in ${fileName}`); @@ -148,6 +125,33 @@ export function getDeclaration( if (!assert(chosenDecl)) { throw new Error(`Symbol ${name} from ${fileName} is a ${ts.SyntaxKind[chosenDecl.kind]}`); } - return chosenDecl; + + // We walk the AST tree looking for a declaration that matches + function walkForDeclaration(rootNode: ts.Node): ts.Declaration|null { + let chosenDecl: ts.Declaration|null = null; + rootNode.forEachChild(node => { + if (chosenDecl !== null) { + return; + } + if (ts.isVariableStatement(node)) { + node.declarationList.declarations.forEach(decl => { + if (bindingNameEquals(decl.name, name)) { + chosenDecl = decl; + } + }); + } else if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) { + if (node.name !== undefined && node.name.text === name) { + chosenDecl = node; + } + } else if ( + ts.isImportDeclaration(node) && node.importClause !== undefined && + node.importClause.name !== undefined && node.importClause.name.text === name) { + chosenDecl = node.importClause; + } else { + chosenDecl = walkForDeclaration(node); + } + }); + return chosenDecl; + } } diff --git a/packages/compiler-cli/src/ngtsc/translator/index.ts b/packages/compiler-cli/src/ngtsc/translator/index.ts index b71e3e7900..44120dae41 100644 --- a/packages/compiler-cli/src/ngtsc/translator/index.ts +++ b/packages/compiler-cli/src/ngtsc/translator/index.ts @@ -6,4 +6,4 @@ * found in the LICENSE file at https://angular.io/license */ -export {ImportManager, translateExpression, translateStatement, translateType} from './src/translator'; +export {Import, ImportManager, NamedImport, translateExpression, translateStatement, translateType} from './src/translator'; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 94f630ae6b..4cef26deac 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -38,6 +38,27 @@ const BINARY_OPERATORS = new Map([ [BinaryOperator.Plus, ts.SyntaxKind.PlusToken], ]); +/** + * Information about an import that has been added to a module. + */ +export interface Import { + /** The name of the module that has been imported. */ + specifier: string; + /** The alias of the imported module. */ + qualifier: string; +} + +/** + * The symbol name and import namespace of an imported symbol, + * which has been registered through the ImportManager. + */ +export interface NamedImport { + /** The import namespace containing this imported symbol. */ + moduleImport: string|null; + /** The (possibly rewritten) name of the imported symbol. */ + symbol: string; +} + export class ImportManager { private specifierToIdentifier = new Map(); private nextIndex = 0; @@ -45,8 +66,7 @@ export class ImportManager { constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') { } - generateNamedImport(moduleName: string, originalSymbol: string): - {moduleImport: string | null, symbol: string} { + generateNamedImport(moduleName: string, originalSymbol: string): NamedImport { // First, rewrite the symbol name. const symbol = this.rewriter.rewriteSymbol(originalSymbol, moduleName); @@ -67,7 +87,7 @@ export class ImportManager { return {moduleImport, symbol}; } - getAllImports(contextPath: string): {specifier: string, qualifier: string}[] { + getAllImports(contextPath: string): Import[] { const imports: {specifier: string, qualifier: string}[] = []; this.specifierToIdentifier.forEach((qualifier, specifier) => { specifier = this.rewriter.rewriteSpecifier(specifier, contextPath); @@ -244,10 +264,10 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor } } - visitConditionalExpr(ast: ConditionalExpr, context: Context): ts.ParenthesizedExpression { - return ts.createParen(ts.createConditional( + visitConditionalExpr(ast: ConditionalExpr, context: Context): ts.ConditionalExpression { + return ts.createConditional( ast.condition.visitExpression(this, context), ast.trueCase.visitExpression(this, context), - ast.falseCase !.visitExpression(this, context))); + ast.falseCase !.visitExpression(this, context)); } visitNotExpr(ast: NotExpr, context: Context): ts.PrefixUnaryExpression { @@ -276,10 +296,9 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor if (!BINARY_OPERATORS.has(ast.operator)) { throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`); } - const binEx = ts.createBinary( + return ts.createBinary( ast.lhs.visitExpression(this, context), BINARY_OPERATORS.get(ast.operator) !, ast.rhs.visitExpression(this, context)); - return ast.parens ? ts.createParen(binEx) : binEx; } visitReadPropExpr(ast: ReadPropExpr, context: Context): ts.PropertyAccessExpression { @@ -493,12 +512,3 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { return ts.createTypeQueryNode(expr as ts.Identifier); } } - -function entityNameToExpr(entity: ts.EntityName): ts.Expression { - if (ts.isIdentifier(entity)) { - return entity; - } - const {left, right} = entity; - const leftExpr = ts.isIdentifier(left) ? left : entityNameToExpr(left); - return ts.createPropertyAccess(leftExpr, right); -} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts index 64f2046980..5d251b5fd1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts @@ -10,6 +10,7 @@ import {BoundTarget, DirectiveMeta} from '@angular/compiler'; import * as ts from 'typescript'; import {Reference} from '../../imports'; +import {TemplateGuardMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; /** @@ -19,7 +20,7 @@ import {ClassDeclaration} from '../../reflection'; export interface TypeCheckableDirectiveMeta extends DirectiveMeta { ref: Reference; queries: string[]; - ngTemplateGuards: string[]; + ngTemplateGuards: TemplateGuardMeta[]; hasNgTemplateContextGuard: boolean; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index ffc1643ab5..9228824a4a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, Binary, Conditional, Interpolation, KeyedRead, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, SafeMethodCall, SafePropertyRead} from '@angular/compiler'; +import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler'; import * as ts from 'typescript'; import {TypeCheckingConfig} from './api'; @@ -42,30 +42,87 @@ const BINARY_OPS = new Map([ export function astToTypescript( ast: AST, maybeResolve: (ast: AST) => ts.Expression | null, config: TypeCheckingConfig): ts.Expression { - const resolved = maybeResolve(ast); - if (resolved !== null) { - return resolved; + const translator = new AstTranslator(maybeResolve, config); + return translator.translate(ast); +} + +class AstTranslator implements AstVisitor { + constructor( + private maybeResolve: (ast: AST) => ts.Expression | null, + private config: TypeCheckingConfig) {} + + translate(ast: AST): ts.Expression { + // Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`, + // which would prevent any custom resolution through `maybeResolve` for that node. + if (ast instanceof ASTWithSource) { + ast = ast.ast; + } + + // First attempt to let any custom resolution logic provide a translation for the given node. + const resolved = this.maybeResolve(ast); + if (resolved !== null) { + return resolved; + } + + return ast.visit(this); } - // Branch based on the type of expression being processed. - if (ast instanceof ASTWithSource) { - // Fall through to the underlying AST. - return astToTypescript(ast.ast, maybeResolve, config); - } else if (ast instanceof PropertyRead) { - // This is a normal property read - convert the receiver to an expression and emit the correct - // TypeScript expression to read the property. - const receiver = astToTypescript(ast.receiver, maybeResolve, config); - return ts.createPropertyAccess(receiver, ast.name); - } else if (ast instanceof Interpolation) { - return astArrayToExpression(ast.expressions, maybeResolve, config); - } else if (ast instanceof Binary) { - const lhs = astToTypescript(ast.left, maybeResolve, config); - const rhs = astToTypescript(ast.right, maybeResolve, config); + + visitBinary(ast: Binary): ts.Expression { + const lhs = this.translate(ast.left); + const rhs = this.translate(ast.right); const op = BINARY_OPS.get(ast.operation); if (op === undefined) { throw new Error(`Unsupported Binary.operation: ${ast.operation}`); } return ts.createBinary(lhs, op as any, rhs); - } else if (ast instanceof LiteralPrimitive) { + } + + visitChain(ast: Chain): never { throw new Error('Method not implemented.'); } + + visitConditional(ast: Conditional): ts.Expression { + const condExpr = this.translate(ast.condition); + const trueExpr = this.translate(ast.trueExp); + const falseExpr = this.translate(ast.falseExp); + return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr)); + } + + visitFunctionCall(ast: FunctionCall): never { throw new Error('Method not implemented.'); } + + visitImplicitReceiver(ast: ImplicitReceiver): never { + throw new Error('Method not implemented.'); + } + + visitInterpolation(ast: Interpolation): ts.Expression { + // Build up a chain of binary + operations to simulate the string concatenation of the + // interpolation's expressions. The chain is started using an actual string literal to ensure + // the type is inferred as 'string'. + return ast.expressions.reduce( + (lhs, ast) => ts.createBinary(lhs, ts.SyntaxKind.PlusToken, this.translate(ast)), + ts.createLiteral('')); + } + + visitKeyedRead(ast: KeyedRead): ts.Expression { + const receiver = this.translate(ast.obj); + const key = this.translate(ast.key); + return ts.createElementAccess(receiver, key); + } + + visitKeyedWrite(ast: KeyedWrite): never { throw new Error('Method not implemented.'); } + + visitLiteralArray(ast: LiteralArray): ts.Expression { + const elements = ast.expressions.map(expr => this.translate(expr)); + return ts.createArrayLiteral(elements); + } + + visitLiteralMap(ast: LiteralMap): ts.Expression { + const properties = ast.keys.map(({key}, idx) => { + const value = this.translate(ast.values[idx]); + return ts.createPropertyAssignment(ts.createStringLiteral(key), value); + }); + return ts.createObjectLiteral(properties, true); + } + + visitLiteralPrimitive(ast: LiteralPrimitive): ts.Expression { if (ast.value === undefined) { return ts.createIdentifier('undefined'); } else if (ast.value === null) { @@ -73,74 +130,60 @@ export function astToTypescript( } else { return ts.createLiteral(ast.value); } - } else if (ast instanceof MethodCall) { - const receiver = astToTypescript(ast.receiver, maybeResolve, config); + } + + visitMethodCall(ast: MethodCall): ts.Expression { + const receiver = this.translate(ast.receiver); const method = ts.createPropertyAccess(receiver, ast.name); - const args = ast.args.map(expr => astToTypescript(expr, maybeResolve, config)); + const args = ast.args.map(expr => this.translate(expr)); return ts.createCall(method, undefined, args); - } else if (ast instanceof Conditional) { - const condExpr = astToTypescript(ast.condition, maybeResolve, config); - const trueExpr = astToTypescript(ast.trueExp, maybeResolve, config); - const falseExpr = astToTypescript(ast.falseExp, maybeResolve, config); - return ts.createParen(ts.createConditional(condExpr, trueExpr, falseExpr)); - } else if (ast instanceof LiteralArray) { - const elements = ast.expressions.map(expr => astToTypescript(expr, maybeResolve, config)); - return ts.createArrayLiteral(elements); - } else if (ast instanceof LiteralMap) { - const properties = ast.keys.map(({key}, idx) => { - const value = astToTypescript(ast.values[idx], maybeResolve, config); - return ts.createPropertyAssignment(ts.createStringLiteral(key), value); - }); - return ts.createObjectLiteral(properties, true); - } else if (ast instanceof KeyedRead) { - const receiver = astToTypescript(ast.obj, maybeResolve, config); - const key = astToTypescript(ast.key, maybeResolve, config); - return ts.createElementAccess(receiver, key); - } else if (ast instanceof NonNullAssert) { - const expr = astToTypescript(ast.expression, maybeResolve, config); + } + + visitNonNullAssert(ast: NonNullAssert): ts.Expression { + const expr = this.translate(ast.expression); return ts.createNonNullExpression(expr); - } else if (ast instanceof PrefixNot) { - return ts.createLogicalNot(astToTypescript(ast.expression, maybeResolve, config)); - } else if (ast instanceof SafePropertyRead) { + } + + visitPipe(ast: BindingPipe): never { throw new Error('Method not implemented.'); } + + visitPrefixNot(ast: PrefixNot): ts.Expression { + return ts.createLogicalNot(this.translate(ast.expression)); + } + + visitPropertyRead(ast: PropertyRead): ts.Expression { + // This is a normal property read - convert the receiver to an expression and emit the correct + // TypeScript expression to read the property. + const receiver = this.translate(ast.receiver); + return ts.createPropertyAccess(receiver, ast.name); + } + + visitPropertyWrite(ast: PropertyWrite): never { throw new Error('Method not implemented.'); } + + visitQuote(ast: Quote): never { throw new Error('Method not implemented.'); } + + visitSafeMethodCall(ast: SafeMethodCall): ts.Expression { + // See the comment in SafePropertyRead above for an explanation of the need for the non-null + // assertion here. + const receiver = this.translate(ast.receiver); + const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); + const args = ast.args.map(expr => this.translate(expr)); + const expr = ts.createCall(method, undefined, args); + const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; + return safeTernary(receiver, expr, whenNull); + } + + visitSafePropertyRead(ast: SafePropertyRead): ts.Expression { // A safe property expression a?.b takes the form `(a != null ? a!.b : whenNull)`, where // whenNull is either of type 'any' or or 'undefined' depending on strictness. The non-null // assertion is necessary because in practice 'a' may be a method call expression, which won't // have a narrowed type when repeated in the ternary true branch. - const receiver = astToTypescript(ast.receiver, maybeResolve, config); + const receiver = this.translate(ast.receiver); const expr = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); - const whenNull = config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; + const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; return safeTernary(receiver, expr, whenNull); - } else if (ast instanceof SafeMethodCall) { - const receiver = astToTypescript(ast.receiver, maybeResolve, config); - // See the comment in SafePropertyRead above for an explanation of the need for the non-null - // assertion here. - const method = ts.createPropertyAccess(ts.createNonNullExpression(receiver), ast.name); - const args = ast.args.map(expr => astToTypescript(expr, maybeResolve, config)); - const expr = ts.createCall(method, undefined, args); - const whenNull = config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; - return safeTernary(receiver, expr, whenNull); - } else { - throw new Error(`Unknown node type: ${Object.getPrototypeOf(ast).constructor}`); } } -/** - * Convert an array of `AST` expressions into a single `ts.Expression`, by converting them all - * and separating them with commas. - */ -function astArrayToExpression( - astArray: AST[], maybeResolve: (ast: AST) => ts.Expression | null, - config: TypeCheckingConfig): ts.Expression { - // Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a - // `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as - // it will be modified during the reduction. - const asts = astArray.slice(); - return asts.reduce( - (lhs, ast) => ts.createBinary( - lhs, ts.SyntaxKind.CommaToken, astToTypescript(ast, maybeResolve, config)), - astToTypescript(asts.pop() !, maybeResolve, config)); -} - function safeTernary( lhs: ts.Expression, whenNotNull: ts.Expression, whenNull: ts.Expression): ts.Expression { const notNullComp = ts.createBinary(lhs, ts.SyntaxKind.ExclamationEqualsToken, ts.createNull()); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 0e5160830a..cb568f9911 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -122,7 +122,7 @@ class TcbVariableOp extends TcbOp { const id = this.tcb.allocateId(); const initializer = ts.createPropertyAccess( /* expression */ ctx, - /* name */ this.variable.value); + /* name */ this.variable.value || '$implicit'); // Declare the variable, and return its identifier. this.scope.addStatement(tsCreateVariable(id, initializer)); @@ -185,22 +185,28 @@ class TcbTemplateBodyOp extends TcbOp { // There are two kinds of guards. Template guards (ngTemplateGuards) allow type narrowing of // the expression passed to an @Input of the directive. Scan the directive to see if it has // any template guards, and generate them if needed. - dir.ngTemplateGuards.forEach(inputName => { + dir.ngTemplateGuards.forEach(guard => { // For each template guard function on the directive, look for a binding to that input. - const boundInput = this.template.inputs.find(i => i.name === inputName) || + const boundInput = this.template.inputs.find(i => i.name === guard.inputName) || this.template.templateAttrs.find( (i: TmplAstTextAttribute | TmplAstBoundAttribute): i is TmplAstBoundAttribute => - i instanceof TmplAstBoundAttribute && i.name === inputName); + i instanceof TmplAstBoundAttribute && i.name === guard.inputName); if (boundInput !== undefined) { // If there is such a binding, generate an expression for it. const expr = tcbExpression(boundInput.value, this.tcb, this.scope); - // Call the guard function on the directive with the directive instance and that - // expression. - const guardInvoke = tsCallMethod(dirId, `ngTemplateGuard_${inputName}`, [ - dirInstId, - expr, - ]); - directiveGuards.push(guardInvoke); + + if (guard.type === 'binding') { + // Use the binding expression itself as guard. + directiveGuards.push(expr); + } else { + // Call the guard function on the directive with the directive instance and that + // expression. + const guardInvoke = tsCallMethod(dirId, `ngTemplateGuard_${guard.inputName}`, [ + dirInstId, + expr, + ]); + directiveGuards.push(guardInvoke); + } } }); @@ -281,6 +287,19 @@ class TcbDirectiveOp extends TcbOp { } } +/** + * Mapping between attributes names that don't correspond to their element property names. + * Note: this mapping has to be kept in sync with the equally named mapping in the runtime. + */ +const ATTR_TO_PROP: {[name: string]: string} = { + 'class': 'className', + 'for': 'htmlFor', + 'formaction': 'formAction', + 'innerHtml': 'innerHTML', + 'readonly': 'readOnly', + 'tabindex': 'tabIndex', +}; + /** * A `TcbOp` which generates code to check "unclaimed inputs" - bindings on an element which were * not attributed to any directive or component, and are instead processed against the HTML element @@ -318,7 +337,8 @@ class TcbUnclaimedInputsOp extends TcbOp { if (binding.type === BindingType.Property) { if (binding.name !== 'style' && binding.name !== 'class') { // A direct binding to a property. - const prop = ts.createPropertyAccess(elId, binding.name); + const propertyName = ATTR_TO_PROP[binding.name] || binding.name; + const prop = ts.createPropertyAccess(elId, propertyName); const assign = ts.createBinary(prop, ts.SyntaxKind.EqualsToken, expr); this.scope.addStatement(ts.createStatement(assign)); } else { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 4c5c1af190..d215831881 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -18,7 +18,7 @@ import {generateTypeCheckBlock} from '../src/type_check_block'; describe('type check blocks', () => { it('should generate a basic block for a binding', - () => { expect(tcb('{{hello}}')).toContain('ctx.hello;'); }); + () => { expect(tcb('{{hello}} {{world}}')).toContain('"" + ctx.hello + ctx.world;'); }); it('should generate literal map expressions', () => { const TEMPLATE = '{{ method({foo: a, bar: b}) }}'; @@ -32,7 +32,7 @@ describe('type check blocks', () => { it('should handle non-null assertions', () => { const TEMPLATE = `{{a!}}`; - expect(tcb(TEMPLATE)).toContain('ctx.a!;'); + expect(tcb(TEMPLATE)).toContain('(ctx.a!);'); }); it('should handle keyed property access', () => { @@ -40,12 +40,27 @@ describe('type check blocks', () => { expect(tcb(TEMPLATE)).toContain('ctx.a[ctx.b];'); }); + it('should translate unclaimed bindings to their property equivalent', () => { + const TEMPLATE = ``; + expect(tcb(TEMPLATE)).toContain('_t1.htmlFor = "test";'); + }); + + it('should handle implicit vars on ng-template', () => { + const TEMPLATE = ``; + expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;'); + }); + + it('should handle implicit vars when using microsyntax', () => { + const TEMPLATE = `
`; + expect(tcb(TEMPLATE)).toContain('var _t2 = _t1.$implicit;'); + }); + it('should generate a forward element reference correctly', () => { const TEMPLATE = ` {{ i.value }} `; - expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); _t1.value;'); + expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); "" + _t1.value;'); }); it('should generate a forward directive reference correctly', () => { @@ -61,7 +76,7 @@ describe('type check blocks', () => { }]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t1 = Dir.ngTypeCtor({}); _t1.value; var _t2 = document.createElement("div");'); + 'var _t1 = Dir.ngTypeCtor({}); "" + _t1.value; var _t2 = document.createElement("div");'); }); it('should handle style and class bindings specially', () => { @@ -82,6 +97,40 @@ describe('type check blocks', () => { expect(block).toContain('(ctx.a as any);'); }); + describe('template guards', () => { + it('should emit invocation guards', () => { + const DIRECTIVES: TestDeclaration[] = [{ + type: 'directive', + name: 'NgIf', + selector: '[ngIf]', + inputs: {'ngIf': 'ngIf'}, + ngTemplateGuards: [{ + inputName: 'ngIf', + type: 'invocation', + }] + }]; + const TEMPLATE = `
`; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('if (NgIf.ngTemplateGuard_ngIf(_t1, ctx.person))'); + }); + + it('should emit binding guards', () => { + const DIRECTIVES: TestDeclaration[] = [{ + type: 'directive', + name: 'NgIf', + selector: '[ngIf]', + inputs: {'ngIf': 'ngIf'}, + ngTemplateGuards: [{ + inputName: 'ngIf', + type: 'binding', + }] + }]; + const TEMPLATE = `
`; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('if (ctx.person !== null)'); + }); + }); + describe('config', () => { const DIRECTIVES: TestDeclaration[] = [{ type: 'directive', diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 695b5dac10..c08b50c3e8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports'; import {AbsoluteFsPath, LogicalFileSystem} from '../../path'; -import {isNamedClassDeclaration} from '../../reflection'; +import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getRootDirs} from '../../util/src/typescript'; import {TypeCheckingConfig} from '../src/api'; @@ -54,7 +54,8 @@ TestClass.ngTypeCtor({value: 'test'}); const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy(program, checker, options, host), + new AbsoluteModuleStrategy( + program, checker, options, host, new TypeScriptReflectionHost(checker)), new LogicalProjectStrategy(checker, logicalFs), ]); const ctx = new TypeCheckContext( @@ -84,7 +85,8 @@ TestClass.ngTypeCtor({value: 'test'}); const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy(program, checker, options, host), + new AbsoluteModuleStrategy( + program, checker, options, host, new TypeScriptReflectionHost(checker)), new LogicalProjectStrategy(checker, logicalFs), ]); const ctx = new TypeCheckContext( diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index d070e729b2..0997dfd720 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -70,7 +70,7 @@ export function createPerformWatchHost( const watcher = chokidar.watch(options.basePath, { // ignore .dotfiles, .js and .map files. // can't ignore other files as we e.g. want to recompile if an `.html` file changes as well. - ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/, + ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json|node_modules)/, ignoreInitial: true, persistent: true, }); diff --git a/packages/compiler-cli/test/compliance/mock_compile.ts b/packages/compiler-cli/test/compliance/mock_compile.ts index ec39a099c8..332f1d9e59 100644 --- a/packages/compiler-cli/test/compliance/mock_compile.ts +++ b/packages/compiler-cli/test/compliance/mock_compile.ts @@ -17,12 +17,12 @@ import {NgtscProgram} from '../../src/ngtsc/program'; const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; const OPERATOR = /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./; -const STRING = /'[^']*'|"[^"]*"|`[\s\S]*?`/; +const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"|`(\\`[\s\S])*?`/; const NUMBER = /\d+/; const ELLIPSIS = '…'; const TOKEN = new RegExp( - `\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})`, + `\\s*((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`, 'y'); type Piece = string | RegExp; @@ -35,10 +35,11 @@ function tokenize(text: string): Piece[] { TOKEN.lastIndex = 0; let match: RegExpMatchArray|null; + let tokenizedTextEnd = 0; const pieces: Piece[] = []; while ((match = TOKEN.exec(text)) !== null) { - const token = match[1]; + const [fullMatch, token] = match; if (token === 'IDENT') { pieces.push(IDENTIFIER); } else if (token === ELLIPSIS) { @@ -46,12 +47,17 @@ function tokenize(text: string): Piece[] { } else { pieces.push(token); } + tokenizedTextEnd += fullMatch.length; } - if (pieces.length === 0 || TOKEN.lastIndex !== 0) { - const from = TOKEN.lastIndex; + if (pieces.length === 0 || tokenizedTextEnd < text.length) { + // The new token that could not be found is located after the + // last tokenized character. + const from = tokenizedTextEnd; const to = from + ERROR_CONTEXT_WIDTH; - throw Error(`Invalid test, no token found for '${text.substr(from, to)}...'`); + throw Error( + `Invalid test, no token found for "${text[tokenizedTextEnd]}" ` + + `(context = '${text.substr(from, to)}...'`); } return pieces; diff --git a/packages/compiler-cli/test/compliance/mock_compiler_spec.ts b/packages/compiler-cli/test/compliance/mock_compiler_spec.ts index 94d69b2780..a1422aa4cc 100644 --- a/packages/compiler-cli/test/compliance/mock_compiler_spec.ts +++ b/packages/compiler-cli/test/compliance/mock_compiler_spec.ts @@ -82,6 +82,34 @@ describe('mock_compiler', () => { result.source, 'name \n\n . \n length', 'name length expression not found (whitespace)'); }); + + it('should throw if the expected output contains unknown characters', () => { + const files = { + app: { + 'test.ts': `ɵsayHello();`, + } + }; + + const result = compile(files, angularFiles); + + expect(() => { + expectEmit(result.source, `ΔsayHello();`, 'Output does not match.'); + }).toThrowError(/Invalid test, no token found for "Δ"/); + }); + + it('should be able to properly handle string literals with escaped quote', () => { + const files = { + app: { + 'test.ts': String.raw `const identifier = "\"quoted\"";`, + } + }; + + const result = compile(files, angularFiles); + + expect(() => { + expectEmit(result.source, String.raw `const $a$ = "\"quoted\"";`, 'Output does not match.'); + }).not.toThrow(); + }); }); it('should be able to skip untested regions (… and // ...)', () => { diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 6ad1ff8c36..82b4379423 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -373,10 +373,10 @@ describe('compiler compliance', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵproperty("ternary", (ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$)); + $r3$.ɵɵproperty("ternary", ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$); $r3$.ɵɵproperty("pipe", $r3$.ɵɵpipeBind3(1, 4, ctx.value, 1, 2)); - $r3$.ɵɵproperty("and", (ctx.cond && $r3$.ɵɵpureFunction1(10, $c0$, ctx.b))); - $r3$.ɵɵproperty("or", (ctx.cond || $r3$.ɵɵpureFunction1(12, $c0$, ctx.c))); + $r3$.ɵɵproperty("and", ctx.cond && $r3$.ɵɵpureFunction1(10, $c0$, ctx.b)); + $r3$.ɵɵproperty("or", ctx.cond || $r3$.ɵɵpureFunction1(12, $c0$, ctx.c)); } } `; @@ -444,19 +444,17 @@ describe('compiler compliance', () => { $r3$.ɵɵallocHostVars(14); } if (rf & 2) { - $r3$.ɵɵcomponentHostSyntheticProperty(elIndex, "@expansionHeight", - $r3$.ɵɵbind( + $r3$.ɵɵupdateSyntheticHostBinding("@expansionHeight", $r3$.ɵɵpureFunction2(5, $_c1$, ctx.getExpandedState(), $r3$.ɵɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight) ) - ), null, true + , null, true ); - $r3$.ɵɵcomponentHostSyntheticProperty(elIndex, "@expansionWidth", - $r3$.ɵɵbind( + $r3$.ɵɵupdateSyntheticHostBinding("@expansionWidth", $r3$.ɵɵpureFunction2(11, $_c1$, ctx.getExpandedState(), $r3$.ɵɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth) ) - ), null, true + , null, true ); } }, @@ -810,7 +808,7 @@ describe('compiler compliance', () => { const $myComp$ = $r3$.ɵɵnextContext(); const $foo$ = $r3$.ɵɵreference(1); $r3$.ɵɵselect(1); - $r3$.ɵɵtextBinding(1, $r3$.ɵɵinterpolation2("", $myComp$.salutation, " ", $foo$, "")); + $r3$.ɵɵtextInterpolate2("", $myComp$.salutation, " ", $foo$, ""); } } … @@ -1159,7 +1157,7 @@ describe('compiler compliance', () => { type: SimpleComponent, selectors: [["simple"]], factory: function SimpleComponent_Factory(t) { return new (t || SimpleComponent)(); }, - ngContentSelectors: _c0, + ngContentSelectors: $c0$, consts: 2, vars: 0, template: function SimpleComponent_Template(rf, ctx) { @@ -1189,10 +1187,10 @@ describe('compiler compliance', () => { if (rf & 1) { $r3$.ɵɵprojectionDef($c1$); $r3$.ɵɵelementStart(0, "div", $c3$); - $r3$.ɵɵprojection(1, 1); + $r3$.ɵɵprojection(1); $r3$.ɵɵelementEnd(); $r3$.ɵɵelementStart(2, "div", $c4$); - $r3$.ɵɵprojection(3, 2); + $r3$.ɵɵprojection(3, 1); $r3$.ɵɵelementEnd(); } }, @@ -1209,6 +1207,54 @@ describe('compiler compliance', () => { result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); }); + it('should support multi-slot content projection with multiple wildcard slots', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + template: \` + + + + \`, + }) + class Cmp {} + + @NgModule({ declarations: [Cmp] }) + class Module {} + `, + } + }; + + const output = ` + const $c0$ = ["*", [["", "spacer", ""]], "*"]; + const $c1$ = ["*", "[spacer]", "*"]; + … + Cmp.ngComponentDef = $r3$.ɵɵdefineComponent({ + type: Cmp, + selectors: [["ng-component"]], + factory: function Cmp_Factory(t) { return new (t || Cmp)(); }, + ngContentSelectors: $c1$, + consts: 3, + vars: 0, + template: function Cmp_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵprojectionDef($c0$); + i0.ɵɵprojection(0); + i0.ɵɵprojection(1, 1); + i0.ɵɵprojection(2, 2); + } + }, + encapsulation: 2 + }); + `; + + const {source} = compile(files, angularFiles); + expectEmit(source, output, 'Invalid content projection instructions generated'); + }); + it('should support content projection in nested templates', () => { const files = { app: { @@ -1241,7 +1287,7 @@ describe('compiler compliance', () => { const $_c2$ = ["id", "second"]; function Cmp_div_0_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div", $_c2$); - $r3$.ɵɵprojection(1, 1); + $r3$.ɵɵprojection(1); $r3$.ɵɵelementEnd(); } } const $_c3$ = ["id", "third"]; @@ -1255,10 +1301,10 @@ describe('compiler compliance', () => { function Cmp_ng_template_2_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtext(0, " '*' selector: "); - $r3$.ɵɵprojection(1); + $r3$.ɵɵprojection(1, 1); } } - const $_c4$ = [[["span", "title", "tofirst"]]]; + const $_c4$ = [[["span", "title", "tofirst"]], "*"]; … template: function Cmp_Template(rf, ctx) { if (rf & 1) { @@ -1312,31 +1358,31 @@ describe('compiler compliance', () => { const output = ` function Cmp_ng_template_1_ng_template_1_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵprojection(0, 4); + $r3$.ɵɵprojection(0, 3); } } function Cmp_ng_template_1_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵprojection(0, 3); + $r3$.ɵɵprojection(0, 2); $r3$.ɵɵtemplate(1, Cmp_ng_template_1_ng_template_1_Template, 1, 0, "ng-template"); } } function Cmp_ng_template_2_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵtext(0, " '*' selector in a template: "); - $r3$.ɵɵprojection(1); + $r3$.ɵɵprojection(1, 4); } } - const $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]]]; - const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]"]; + const $_c0$ = [[["", "id", "tomainbefore"]], [["", "id", "tomainafter"]], [["", "id", "totemplate"]], [["", "id", "tonestedtemplate"]], "*"]; + const $_c1$ = ["[id=toMainBefore]", "[id=toMainAfter]", "[id=toTemplate]", "[id=toNestedTemplate]", "*"]; … template: function Cmp_Template(rf, ctx) { if (rf & 1) { - $r3$.ɵɵprojectionDef($_c2$); - $r3$.ɵɵprojection(0, 1); + $r3$.ɵɵprojectionDef($_c0$); + $r3$.ɵɵprojection(0); $r3$.ɵɵtemplate(1, Cmp_ng_template_1_Template, 2, 0, "ng-template"); $r3$.ɵɵtemplate(2, Cmp_ng_template_2_Template, 2, 0, "ng-template"); - $r3$.ɵɵprojection(3, 2); + $r3$.ɵɵprojection(3, 1); } } `; @@ -1508,8 +1554,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDirs = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDirs = $tmp$); } }, consts: 1, @@ -1566,8 +1612,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRef = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRefs = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRef = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRefs = $tmp$); } }, … @@ -1619,8 +1665,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.foo = $tmp$.first)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.foo = $tmp$.first); } }, consts: 1, @@ -1684,10 +1730,10 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRef = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRefs = $tmp$)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDirs = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRef = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.myRefs = $tmp$); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.someDirs = $tmp$); } }, … @@ -1714,7 +1760,7 @@ describe('compiler compliance', () => { \` }) export class ContentQueryComponent { - @ContentChild(SomeDirective) someDir: SomeDirective; + @ContentChild(SomeDirective, {static: false}) someDir: SomeDirective; @ContentChildren(SomeDirective) someDirList !: QueryList; } @@ -1748,8 +1794,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDirList = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDirList = $tmp$); } }, ngContentSelectors: _c0, @@ -1786,7 +1832,7 @@ describe('compiler compliance', () => { \` }) export class ContentQueryComponent { - @ContentChild('myRef') myRef: any; + @ContentChild('myRef', {static: false}) myRef: any; @ContentChildren('myRef1, myRef2, myRef3') myRefs: QueryList; } @NgModule({declarations: [ContentQueryComponent]}) @@ -1808,8 +1854,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRef = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRefs = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRef = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRefs = $tmp$); } }, … @@ -1870,8 +1916,8 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.foo = $tmp$.first)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.foo = $tmp$.first); } }, ngContentSelectors: $_c1$, @@ -1911,9 +1957,9 @@ describe('compiler compliance', () => { \` }) export class ContentQueryComponent { - @ContentChild('myRef', {read: TemplateRef}) myRef: TemplateRef; + @ContentChild('myRef', {read: TemplateRef, static: false}) myRef: TemplateRef; @ContentChildren('myRef1, myRef2, myRef3', {read: ElementRef}) myRefs: QueryList; - @ContentChild(SomeDirective, {read: ElementRef}) someDir: ElementRef; + @ContentChild(SomeDirective, {read: ElementRef, static: false}) someDir: ElementRef; @ContentChildren(SomeDirective, {read: TemplateRef}) someDirs: QueryList; } @NgModule({declarations: [ContentQueryComponent]}) @@ -1937,10 +1983,10 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRef = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRefs = $tmp$)); - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDirs = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRef = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDir = $tmp$.first); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.myRefs = $tmp$); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.someDirs = $tmp$); } }, … @@ -1966,7 +2012,7 @@ describe('compiler compliance', () => { \` }) export class ContentQueryComponent { - @Input() @ContentChild('foo') foo: any; + @Input() @ContentChild('foo', {static: false}) foo: any; } @Component({ @@ -2074,9 +2120,9 @@ describe('compiler compliance', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵtextBinding(0, $r3$.ɵɵinterpolation1("", $r3$.ɵɵpipeBind2(1, 3, $r3$.ɵɵpipeBind2(2, 6, ctx.name, ctx.size), ctx.size), "")); + $r3$.ɵɵtextInterpolate($r3$.ɵɵpipeBind2(1, 3, $r3$.ɵɵpipeBind2(2, 6, ctx.name, ctx.size), ctx.size)); $r3$.ɵɵselect(4); - $r3$.ɵɵtextBinding(4, $r3$.ɵɵinterpolation2("", $r3$.ɵɵpipeBindV(5, 9, $r3$.ɵɵpureFunction1(18, $c0$, ctx.name)), " ", (ctx.name ? 1 : $r3$.ɵɵpipeBind1(6, 16, 2)), "")); + $r3$.ɵɵtextInterpolate2("", $r3$.ɵɵpipeBindV(5, 9, $r3$.ɵɵpureFunction1(18, $c0$, ctx.name)), " ", ctx.name ? 1 : $r3$.ɵɵpipeBind1(6, 16, 2), ""); } }, pipes: [MyPurePipe, MyPipe], @@ -2139,14 +2185,14 @@ describe('compiler compliance', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵtextBinding(0, $r3$.ɵɵinterpolation5( + $r3$.ɵɵtextInterpolate5( "0:", i0.ɵɵpipeBind1(1, 5, ctx.name), "1:", i0.ɵɵpipeBind2(2, 7, ctx.name, 1), "2:", i0.ɵɵpipeBind3(3, 10, ctx.name, 1, 2), "3:", i0.ɵɵpipeBind4(4, 14, ctx.name, 1, 2, 3), "4:", i0.ɵɵpipeBindV(5, 19, $r3$.ɵɵpureFunction1(25, $c0$, ctx.name)), "" - )); + ); } }, pipes: [MyPipe], @@ -2192,7 +2238,7 @@ describe('compiler compliance', () => { if (rf & 2) { const $user$ = $r3$.ɵɵreference(1); $r3$.ɵɵselect(2); - $r3$.ɵɵtextBinding(2, $r3$.ɵɵinterpolation1("Hello ", $user$.value, "!")); + $r3$.ɵɵtextInterpolate1("Hello ", $user$.value, "!"); } }, encapsulation: 2 @@ -2255,7 +2301,7 @@ describe('compiler compliance', () => { const $foo$ = $r3$.ɵɵreference(1); const $baz$ = $r3$.ɵɵreference(5); $r3$.ɵɵselect(1); - $r3$.ɵɵtextBinding(1, $r3$.ɵɵinterpolation3("", $foo$, "-", $bar$, "-", $baz$, "")); + $r3$.ɵɵtextInterpolate3("", $foo$, "-", $bar$, "-", $baz$, ""); } } function MyComponent_div_3_Template(rf, ctx) { @@ -2271,7 +2317,7 @@ describe('compiler compliance', () => { $r3$.ɵɵnextContext(); const $foo$ = $r3$.ɵɵreference(1); $r3$.ɵɵselect(1); - $r3$.ɵɵtextBinding(1, $r3$.ɵɵinterpolation2(" ", $foo$, "-", $bar$, " ")); + $r3$.ɵɵtextInterpolate2(" ", $foo$, "-", $bar$, " "); } } … @@ -2291,7 +2337,7 @@ describe('compiler compliance', () => { if (rf & 2) { const $foo$ = $r3$.ɵɵreference(1); $r3$.ɵɵselect(2); - $r3$.ɵɵtextBinding(2, $r3$.ɵɵinterpolation1(" ", $foo$, " ")); + $r3$.ɵɵtextInterpolate1(" ", $foo$, " "); } }, directives:[IfDirective], @@ -2300,9 +2346,7 @@ describe('compiler compliance', () => { const result = compile(files, angularFiles); const source = result.source; - expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); - }); it('should support local refs mixed with context assignments', () => { @@ -2344,7 +2388,7 @@ describe('compiler compliance', () => { const $item$ = $i0$.ɵɵnextContext().$implicit; const $foo$ = $i0$.ɵɵreference(2); $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation2(" ", $foo$, " - ", $item$, " ")); + $i0$.ɵɵtextInterpolate2(" ", $foo$, " - ", $item$, " "); } } @@ -2648,7 +2692,7 @@ describe('compiler compliance', () => { if (rf & 2) { const $item$ = ctx.$implicit; $r3$.ɵɵselect(1); - $r3$.ɵɵtextBinding(1, $r3$.ɵɵinterpolation1("", $item$.name, "")); + $r3$.ɵɵtextInterpolate($item$.name); } } … @@ -2731,7 +2775,7 @@ describe('compiler compliance', () => { const $info$ = ctx.$implicit; const $item$ = $r3$.ɵɵnextContext().$implicit; $r3$.ɵɵselect(1); - $r3$.ɵɵtextBinding(1, $r3$.ɵɵinterpolation2(" ", $item$.name, ": ", $info$.description, " ")); + $r3$.ɵɵtextInterpolate2(" ", $item$.name, ": ", $info$.description, " "); } } @@ -2749,7 +2793,7 @@ describe('compiler compliance', () => { if (rf & 2) { const $item$ = ctx.$implicit; $r3$.ɵɵselect(2); - $r3$.ɵɵtextBinding(2, $r3$.ɵɵinterpolation1("", IDENT.name, "")); + $r3$.ɵɵtextInterpolate(IDENT.name); $r3$.ɵɵselect(4); $r3$.ɵɵproperty("forOf", IDENT.infos); } @@ -3078,7 +3122,7 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$.first)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$.first); } } }); @@ -3123,7 +3167,7 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadViewQuery())) && (ctx.something = $tmp$); } } }); @@ -3139,7 +3183,7 @@ describe('compiler compliance', () => { 'spec.ts': ` import {Component, NgModule, ContentChild} from '@angular/core'; export class BaseClass { - @ContentChild('something') something: any; + @ContentChild('something', {static: false}) something: any; } @Component({ @@ -3166,7 +3210,7 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$.first)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$.first); } } }); @@ -3211,7 +3255,7 @@ describe('compiler compliance', () => { } if (rf & 2) { var $tmp$; - ($r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$)); + $r3$.ɵɵqueryRefresh(($tmp$ = $r3$.ɵɵloadContentQuery())) && (ctx.something = $tmp$); } } }); @@ -3253,7 +3297,7 @@ describe('compiler compliance', () => { $r3$.ɵɵallocHostVars(1); } if (rf & 2) { - $r3$.ɵɵelementAttribute(elIndex, "tabindex", $r3$.ɵɵbind(ctx.tabindex)); + $r3$.ɵɵattribute("tabindex", ctx.tabindex); } } }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index 7fa4977f60..025eaada28 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -45,7 +45,7 @@ describe('compiler compliance: bindings', () => { } if (rf & 2) { $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation1("Hello ", $ctx$.name, "")); + $i0$.ɵɵtextInterpolate1("Hello ", $ctx$.name, ""); } }`; const result = compile(files, angularFiles); @@ -308,7 +308,7 @@ describe('compiler compliance: bindings', () => { $r3$.ɵɵallocHostVars(1); } if (rf & 2) { - $r3$.ɵɵelementAttribute(elIndex, "required", $r3$.ɵɵbind(ctx.required)); + $r3$.ɵɵattribute("required", ctx.required); } } }); @@ -496,6 +496,51 @@ describe('compiler compliance: bindings', () => { expectEmit(result.source, template, 'Incorrect handling of interpolated properties'); }); + + it('should generate the proper update instructions for interpolated attributes', () => { + const files: MockDirectory = getAppFiles(` +
+
+
+
+
+
+
+
+
+
+ `); + + const template = ` + … + if (rf & 2) { + i0.ɵɵselect(0); + i0.ɵɵattributeInterpolateV("title", ["a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i", ctx.nine, "j"]); + i0.ɵɵselect(1); + i0.ɵɵattributeInterpolate8("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h", ctx.eight, "i"); + i0.ɵɵselect(2); + i0.ɵɵattributeInterpolate7("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g", ctx.seven, "h"); + i0.ɵɵselect(3); + i0.ɵɵattributeInterpolate6("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f", ctx.six, "g"); + i0.ɵɵselect(4); + i0.ɵɵattributeInterpolate5("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e", ctx.five, "f"); + i0.ɵɵselect(5); + i0.ɵɵattributeInterpolate4("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d", ctx.four, "e"); + i0.ɵɵselect(6); + i0.ɵɵattributeInterpolate3("title", "a", ctx.one, "b", ctx.two, "c", ctx.three, "d"); + i0.ɵɵselect(7); + i0.ɵɵattributeInterpolate2("title", "a", ctx.one, "b", ctx.two, "c"); + i0.ɵɵselect(8); + i0.ɵɵattributeInterpolate1("title", "a", ctx.one, "b"); + i0.ɵɵselect(9); + i0.ɵɵattribute("title", ctx.one); + } + … + `; + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect handling of interpolated properties'); + }); + it('should keep local ref for host element', () => { const files: MockDirectory = getAppFiles(` @@ -522,7 +567,7 @@ describe('compiler compliance: bindings', () => { if (rf & 2) { const $_r0$ = $i0$.ɵɵreference(1); $r3$.ɵɵselect(4); - $i0$.ɵɵtextBinding(4, $i0$.ɵɵinterpolation1(" ", $_r0$.id, " ")); + $i0$.ɵɵtextInterpolate1(" ", $_r0$.id, " "); } } `; diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index fbd0bc5f2e..6f5a741f96 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -197,7 +197,7 @@ describe('i18n support in the view compiler', () => { else { $I18N_0$ = $r3$.ɵɵi18nLocalize("Content A"); } - const $_c2$ = ["title", "Title B"]; + const $_c2$ = [${AttributeMarker.I18n}, "title"]; var $I18N_3$; if (ngI18nClosureMode) { /** @@ -211,7 +211,6 @@ describe('i18n support in the view compiler', () => { $I18N_3$ = $r3$.ɵɵi18nLocalize("Title B"); } const $_c5$ = ["title", $I18N_3$]; - const $_c6$ = ["title", "Title C"]; var $I18N_7$; if (ngI18nClosureMode) { /** @@ -224,7 +223,6 @@ describe('i18n support in the view compiler', () => { $I18N_7$ = $r3$.ɵɵi18nLocalize("Title C"); } const $_c9$ = ["title", $I18N_7$]; - const $_c10$ = ["title", "Title D"]; var $I18N_11$; if (ngI18nClosureMode) { /** @@ -238,7 +236,6 @@ describe('i18n support in the view compiler', () => { $I18N_11$ = $r3$.ɵɵi18nLocalize("Title D"); } const $_c13$ = ["title", $I18N_11$]; - const $_c14$ = ["title", "Title E"]; var $I18N_15$; if (ngI18nClosureMode) { /** @@ -251,7 +248,6 @@ describe('i18n support in the view compiler', () => { $I18N_15$ = $r3$.ɵɵi18nLocalize("Title E"); } const $_c17$ = ["title", $I18N_15$]; - const $_c18$ = ["title", "Title F"]; var $I18N_19$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_idF$$APP_SPEC_TS_20$ = goog.getMsg("Title F"); @@ -261,7 +257,6 @@ describe('i18n support in the view compiler', () => { $I18N_19$ = $r3$.ɵɵi18nLocalize("Title F"); } const $_c21$ = ["title", $I18N_19$]; - const $_c22$ = ["title", "Title G"]; var $I18N_23$; if (ngI18nClosureMode) { /** @@ -335,7 +330,7 @@ describe('i18n support in the view compiler', () => { `; const output = ` - const $_c0$ = ["id", "static", "title", "introduction"]; + const $_c0$ = ["id", "static", ${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -376,7 +371,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["id", "dynamic-1", "aria-roledescription", "static text", ${AttributeMarker.Bindings}, "title", "aria-label"]; + const $_c0$ = ["id", "dynamic-1", ${AttributeMarker.I18n}, "aria-roledescription", "title", "aria-label"]; var $I18N_1$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_5526535577705876535$$APP_SPEC_TS_1$ = goog.getMsg("static text"); @@ -422,7 +417,7 @@ describe('i18n support in the view compiler', () => { "title", $I18N_2$, "aria-label", $I18N_3$ ]; - const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.Bindings}, "title", "aria-roledescription"]; + const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"]; var $I18N_6$; if (ngI18nClosureMode) { /** @@ -470,13 +465,13 @@ describe('i18n support in the view compiler', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 0, ctx.valueA))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 6, ctx.valueA))); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueB)); $r3$.ɵɵi18nApply(2); $r3$.ɵɵselect(3); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueA)); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueB)); - $r3$.ɵɵi18nExp($r3$.ɵɵbind((ctx.valueA + ctx.valueB))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueA + ctx.valueB)); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueC)); $r3$.ɵɵi18nApply(4); } @@ -492,7 +487,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [${AttributeMarker.Bindings}, "title"]; + const $_c0$ = [${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -520,7 +515,7 @@ describe('i18n support in the view compiler', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 0, ctx.valueA))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 1, ctx.valueA))); $r3$.ɵɵi18nApply(2); } } @@ -537,7 +532,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [${AttributeMarker.Template}, "ngFor", "ngForOf"]; - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -567,7 +562,7 @@ describe('i18n support in the view compiler', () => { if (rf & 2) { const $outer_r1$ = ctx.$implicit; $r3$.ɵɵselect(1); - $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(2, 0, $outer_r1$))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(2, 1, $outer_r1$))); $r3$.ɵɵi18nApply(3); } } @@ -603,8 +598,8 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [ - "id", "dynamic-1", "aria-roledescription", "static text", - ${AttributeMarker.Bindings}, "title", "aria-label" + "id", "dynamic-1", + ${AttributeMarker.I18n}, "aria-roledescription", "title", "aria-label" ]; var $I18N_1$; if (ngI18nClosureMode) { @@ -651,7 +646,7 @@ describe('i18n support in the view compiler', () => { "title", $I18N_2$, "aria-label", $I18N_3$ ]; - const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.Bindings}, "title", "aria-roledescription"]; + const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"]; var $I18N_6$; if (ngI18nClosureMode) { /** @@ -699,13 +694,13 @@ describe('i18n support in the view compiler', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 0, ctx.valueA))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(1, 6, ctx.valueA))); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueB)); $r3$.ɵɵi18nApply(2); $r3$.ɵɵselect(3); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueA)); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueB)); - $r3$.ɵɵi18nExp($r3$.ɵɵbind((ctx.valueA + ctx.valueB))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueA + ctx.valueB)); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueC)); $r3$.ɵɵi18nApply(4); } @@ -724,7 +719,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [${AttributeMarker.Template}, "ngFor", "ngForOf"]; - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { /** @@ -754,7 +749,7 @@ describe('i18n support in the view compiler', () => { if (rf & 2) { const $outer_r1$ = ctx.$implicit; $r3$.ɵɵselect(1); - $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(2, 0, $outer_r1$))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(2, 1, $outer_r1$))); $r3$.ɵɵi18nApply(3); } } @@ -781,7 +776,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["title", "Element title"]; + const $_c0$ = [${AttributeMarker.I18n}, "title"]; var $I18N_0$; if (ngI18nClosureMode) { /** @@ -1065,7 +1060,7 @@ describe('i18n support in the view compiler', () => { if (rf & 2) { $r3$.ɵɵselect(1); $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(2, 2, ctx.valueA))); - $r3$.ɵɵi18nExp($r3$.ɵɵbind(((ctx.valueA == null) ? null : ((ctx.valueA.a == null) ? null : ctx.valueA.a.b)))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.valueA == null ? null : ctx.valueA.a == null ? null : ctx.valueA.a.b)); $r3$.ɵɵi18nApply(1); } } @@ -1141,7 +1136,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(4, 3, ctx.two))); $r3$.ɵɵi18nApply(3); $r3$.ɵɵselect(6); - $r3$.ɵɵi18nExp($r3$.ɵɵbind(((ctx.three + ctx.four) + ctx.five))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.three + ctx.four + ctx.five)); $r3$.ɵɵi18nApply(6); } } @@ -1261,7 +1256,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_4782264005467235841$$APP_SPEC_TS_3$ = goog.getMsg("Span title {$interpolation} and {$interpolation_1}", { @@ -1453,7 +1448,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵɵelement(0, "img", $_c0$); } } - const $_c3$ = ["src", "logo.png", ${AttributeMarker.Bindings}, "title"]; + const $_c3$ = ["src", "logo.png", ${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_2367729185105559721$$APP_SPEC_TS__2$ = goog.getMsg("App logo #{$interpolation}", { @@ -1609,7 +1604,7 @@ describe('i18n support in the view compiler', () => { if (rf & 2) { const $ctx_r1$ = $r3$.ɵɵnextContext(); $r3$.ɵɵselect(0); - $r3$.ɵɵi18nExp($r3$.ɵɵbind(($ctx_r1$.valueE + $ctx_r1$.valueF))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind($ctx_r1$.valueE + $ctx_r1$.valueF)); $r3$.ɵɵi18nExp($r3$.ɵɵbind($r3$.ɵɵpipeBind1(3, 2, $ctx_r1$.valueG))); $r3$.ɵɵi18nApply(0); } @@ -2643,6 +2638,7 @@ describe('i18n support in the view compiler', () => { } } `; + verify(input, output, {inputArgs: {interpolation: ['{%', '%}']}}); }); @@ -2691,7 +2687,7 @@ describe('i18n support in the view compiler', () => { "startItalicText": "\uFFFD#4\uFFFD", "closeItalicText": "\uFFFD/#4\uFFFD", "closeTagDiv": "\uFFFD/#3\uFFFD", - "icu": I18N_APP_SPEC_TS_1 + "icu": $I18N_1$ }); $I18N_0$ = $MSG_EXTERNAL_5791551881115084301$$APP_SPEC_TS_0$; } @@ -2703,7 +2699,7 @@ describe('i18n support in the view compiler', () => { "startItalicText": "\uFFFD#4\uFFFD", "closeItalicText": "\uFFFD/#4\uFFFD", "closeTagDiv": "\uFFFD/#3\uFFFD", - "icu": I18N_APP_SPEC_TS_1 + "icu": $I18N_1$ }); } … @@ -2714,14 +2710,14 @@ describe('i18n support in the view compiler', () => { $r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵi18nStart(1, $I18N_0$); $r3$.ɵɵelement(2, "b"); - $r3$.ɵɵelementStart(3, "div"); - $r3$.ɵɵstyling($_c2$); + $r3$.ɵɵelementStart(3, "div", $_c2$); $r3$.ɵɵelement(4, "i"); $r3$.ɵɵelementEnd(); $r3$.ɵɵi18nEnd(); $r3$.ɵɵelementEnd(); } if (rf & 2) { + $r3$.ɵɵselect(1); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.gender)); $r3$.ɵɵi18nApply(1); } @@ -2764,7 +2760,7 @@ describe('i18n support in the view compiler', () => { if (rf & 2) { $r3$.ɵɵselect(1); $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.gender)); - $r3$.ɵɵi18nExp($r3$.ɵɵbind(((ctx.ageA + ctx.ageB) + ctx.ageC))); + $r3$.ɵɵi18nExp($r3$.ɵɵbind(ctx.ageA + ctx.ageB + ctx.ageC)); $r3$.ɵɵi18nApply(1); } } diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts index c7368bfda5..e40d62ad52 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts @@ -48,7 +48,7 @@ describe('compiler compliance: listen()', () => { $r3$.ɵɵelementStart(0, "div", $e0_attrs$); $r3$.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener($event) { ctx.onClick($event); - return (1 == 2); + return 1 == 2; }); $r3$.ɵɵelementEnd(); } diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_providers_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_providers_spec.ts index 0a0eaed14e..e46ac76c9c 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_providers_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_providers_spec.ts @@ -144,9 +144,19 @@ describe('compiler compliance: providers', () => { result.source, ` export class MyComponent { } - MyComponent.ngComponentDef = i0.ɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }, consts: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { - i0.ɵelement(0, "div"); - } } });`, + MyComponent.ngComponentDef = i0.ɵɵdefineComponent({ + type: MyComponent, + selectors: [["my-component"]], + factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }, + consts: 1, + vars: 0, + template: function MyComponent_Template(rf, ctx) { + if (rf & 1) { + i0.ɵɵelement(0, "div"); + } + }, + encapsulation: 2 + });`, 'Incorrect features'); }); }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts index 10dbc48055..57b0e0b71e 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_spec.ts @@ -93,7 +93,7 @@ describe('r3_view_compiler', () => { describe('interpolations', () => { // Regression #21927 - it('should generate a correct call to bV with more than 8 interpolations', () => { + it('should generate a correct call to textInterpolateV with more than 8 interpolations', () => { const files: MockDirectory = { app: { 'example.ts': ` @@ -112,10 +112,19 @@ describe('r3_view_compiler', () => { } }; - const bV_call = - `$r3$.ɵɵinterpolationV([" ",ctx.list[0]," ",ctx.list[1]," ",ctx.list[2]," ",ctx.list[3], - " ",ctx.list[4]," ",ctx.list[5]," ",ctx.list[6]," ",ctx.list[7]," ",ctx.list[8], - " "])`; + const bV_call = ` + … + function MyApp_Template(rf, ctx) { + if (rf & 1) { + $i0$.ɵɵtext(0); + } + if (rf & 2) { + $i0$.ɵɵselect(0); + $i0$.ɵɵtextInterpolateV([" ", ctx.list[0], " ", ctx.list[1], " ", ctx.list[2], " ", ctx.list[3], " ", ctx.list[4], " ", ctx.list[5], " ", ctx.list[6], " ", ctx.list[7], " ", ctx.list[8], " "]); + } + } + … + `; const result = compile(files, angularFiles); expectEmit(result.source, bV_call, 'Incorrect bV call'); }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 250cb6bf56..8ac751f5f9 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -7,7 +7,9 @@ */ import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core'; +import {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state'; import {setup} from '@angular/compiler/test/aot/test_util'; + import {compile, expectEmit} from './mock_compile'; describe('compiler compliance: styling', () => { @@ -348,7 +350,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); }); $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); }); } if (rf & 2) { - $r3$.ɵɵcomponentHostSyntheticProperty(elIndex, "@myAnim", $r3$.ɵɵbind(ctx.myAnimState), null, true); + $r3$.ɵɵupdateSyntheticHostBinding("@myAnim", ctx.myAnimState, null, true); } } … @@ -547,7 +549,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵstyleProp(0, $ctx$.myWidth); $r3$.ɵɵstyleProp(1, $ctx$.myHeight); $r3$.ɵɵstylingApply(); - $r3$.ɵɵelementAttribute(0, "style", $r3$.ɵɵbind("border-width: 10px"), $r3$.ɵɵsanitizeStyle); + $r3$.ɵɵattribute("style", "border-width: 10px", $r3$.ɵɵsanitizeStyle); } }, encapsulation: 2 @@ -772,7 +774,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassProp(0, $ctx$.yesToApple); $r3$.ɵɵclassProp(1, $ctx$.yesToOrange); $r3$.ɵɵstylingApply(); - $r3$.ɵɵelementAttribute(0, "class", $r3$.ɵɵbind("banana")); + $r3$.ɵɵattribute("class", "banana"); } }, encapsulation: 2 @@ -822,8 +824,8 @@ describe('compiler compliance: styling', () => { } if (rf & 2) { $r3$.ɵɵselect(0); - $r3$.ɵɵelementAttribute(0, "class", $r3$.ɵɵbind("round")); - $r3$.ɵɵelementAttribute(0, "style", $r3$.ɵɵbind("height:100px"), $r3$.ɵɵsanitizeStyle); + $r3$.ɵɵattribute("class", "round"); + $r3$.ɵɵattribute("style", "height:100px", $r3$.ɵɵsanitizeStyle); } }, encapsulation: 2 @@ -999,7 +1001,7 @@ describe('compiler compliance: styling', () => { $r3$.ɵɵclassProp(0, $r3$.ɵɵpipeBind2(4, 10, $ctx$.fooExp, 2000)); $r3$.ɵɵstylingApply(); $r3$.ɵɵselect(5); - $r3$.ɵɵtextBinding(5, $r3$.ɵɵinterpolation1(" ", $ctx$.item, "")); + $r3$.ɵɵtextInterpolate1(" ", $ctx$.item, ""); } } `; @@ -1463,4 +1465,122 @@ describe('compiler compliance: styling', () => { const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); }); + + describe('new styling refactor', () => { + beforeEach(() => { compilerSetStylingMode(CompilerStylingMode.UseNew); }); + + afterEach(() => { compilerSetStylingMode(CompilerStylingMode.UseOld); }); + + it('should generate a `styleSanitizer` instruction when one or more sanitizable style properties are statically detected', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-app', + template: \` +
+ \` + }) + export class MyAppComp { + bgExp = ''; + } + ` + } + }; + + const template = ` + template: function MyAppComp_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵselect(0); + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyleProp(0, ctx.bgExp); + $r3$.ɵɵstylingApply(); + } + … + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should generate a `styleSanitizer` instruction when a `styleMap` instruction is used', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-app', + template: \` +
+ \` + }) + export class MyAppComp { + mapExp = {}; + } + ` + } + }; + + const template = ` + template: function MyAppComp_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵselect(0); + $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); + $r3$.ɵɵstyleMap(ctx.mapExp); + $r3$.ɵɵstylingApply(); + } + … + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('shouldn\'t generate a `styleSanitizer` instruction when class-based instructions are used', + () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-app', + template: \` +
+ \` + }) + export class MyAppComp { + mapExp = {}; + nameExp = true; + } + ` + } + }; + + const template = ` + template: function MyAppComp_Template(rf, ctx) { + … + if (rf & 2) { + $r3$.ɵɵselect(0); + $r3$.ɵɵclassMap(ctx.mapExp); + $r3$.ɵɵclassProp(0, ctx.nameExp); + $r3$.ɵɵstylingApply(); + } + … + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + }); }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts index 09d702fc86..0f54733b7f 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_template_spec.ts @@ -78,7 +78,7 @@ describe('compiler compliance: template', () => { $i0$.ɵɵselect(0); $i0$.ɵɵproperty("title", $myComp1$.format($outer1$, $middle1$, $inner1$, $myComp1$.component)); $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation1(" ", $myComp1$.format($outer1$, $middle1$, $inner1$, $myComp1$.component), " ")); + $i0$.ɵɵtextInterpolate1(" ", $myComp1$.format($outer1$, $middle1$, $inner1$, $myComp1$.component), " "); } } @@ -215,7 +215,7 @@ describe('compiler compliance: template', () => { const $item$ = ctx.$implicit; const $i$ = ctx.index; $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation2(" ", $i$, " - ", $item$, " ")); + $i0$.ɵɵtextInterpolate2(" ", $i$, " - ", $item$, " "); } } // ... @@ -272,7 +272,7 @@ describe('compiler compliance: template', () => { const $i$ = $div$.index; const $item$ = $div$.$implicit; $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation2(" ", $i$, " - ", $item$, " ")); + $i0$.ɵɵtextInterpolate2(" ", $i$, " - ", $item$, " "); } } @@ -343,7 +343,7 @@ describe('compiler compliance: template', () => { const $middle$ = $i0$.ɵɵnextContext().$implicit; const $myComp$ = $i0$.ɵɵnextContext(2); $r3$.ɵɵselect(1); - $i0$.ɵɵtextBinding(1, $i0$.ɵɵinterpolation2(" ", $middle$.value, " - ", $myComp$.name, " ")); + $i0$.ɵɵtextInterpolate2(" ", $middle$.value, " - ", $myComp$.name, " "); } } @@ -607,7 +607,7 @@ describe('compiler compliance: template', () => { export class AComponent { show = true; } - + @Component({ selector: 'b-component', template: \` diff --git a/packages/compiler-cli/test/metadata/collector_spec.ts b/packages/compiler-cli/test/metadata/collector_spec.ts index e48d686cf3..5e0b7cec2c 100644 --- a/packages/compiler-cli/test/metadata/collector_spec.ts +++ b/packages/compiler-cli/test/metadata/collector_spec.ts @@ -243,7 +243,7 @@ describe('Collector', () => { version: METADATA_VERSION, metadata: { HEROES: [ - {'id': 11, 'name': 'Mr. Nice', '$quoted$': ['id', 'name']}, + {'id': 11, 'name': 'Dr Nice', '$quoted$': ['id', 'name']}, {'id': 12, 'name': 'Narco', '$quoted$': ['id', 'name']}, {'id': 13, 'name': 'Bombasto', '$quoted$': ['id', 'name']}, {'id': 14, 'name': 'Celeritas', '$quoted$': ['id', 'name']}, @@ -1221,7 +1221,7 @@ const FILES: Directory = { import {Hero as Hero} from './hero'; export const HEROES: Hero[] = [ - {"id": 11, "name": "Mr. Nice"}, + {"id": 11, "name": "Dr Nice"}, {"id": 12, "name": "Narco"}, {"id": 13, "name": "Bombasto"}, {"id": 14, "name": "Celeritas"}, diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index cc2258882a..665bd37c98 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2028,11 +2028,13 @@ describe('ngc transformer command-line', () => { const exitCode = main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message)); expect(exitCode).toBe(1, 'Compile was expected to fail'); + const srcPathWithSep = `lib${path.sep}`; expect(messages[0]) - .toEqual(`lib/test.component.ts(6,21): Error during template compile of 'TestComponent' + .toEqual( + `${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent' Tagged template expressions are not supported in metadata in 't1' - 't1' references 't2' at lib/indirect1.ts(3,27) - 't2' contains the error at lib/indirect2.ts(4,27). + 't1' references 't2' at ${srcPathWithSep}indirect1.ts(3,27) + 't2' contains the error at ${srcPathWithSep}indirect2.ts(4,27). `); }); }); diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 52281fb454..3daec54ab5 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -29,7 +29,7 @@ function setupFakeCore(support: TestSupport): void { const nodeModulesPath = path.join(support.basePath, 'node_modules'); const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core'); - fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'dir'); + fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'junction'); } /** @@ -49,7 +49,7 @@ export class NgtscTestEnvironment { */ static setup(): NgtscTestEnvironment { const support = setup(); - const outDir = path.join(support.basePath, 'built'); + const outDir = path.posix.join(support.basePath, 'built'); process.chdir(support.basePath); setupFakeCore(support); @@ -121,7 +121,7 @@ export class NgtscTestEnvironment { if (this.multiCompileHostExt === null) { throw new Error(`Not tracking written files - call enableMultipleCompilations()`); } - const outDir = path.join(this.support.basePath, 'built'); + const outDir = path.posix.join(this.support.basePath, 'built'); const writtenFiles = new Set(); this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => { if (rawFile.startsWith(outDir)) { @@ -133,7 +133,7 @@ export class NgtscTestEnvironment { write(fileName: string, content: string) { if (this.multiCompileHostExt !== null) { - const absFilePath = path.resolve(this.support.basePath, fileName); + const absFilePath = path.posix.resolve(this.support.basePath, fileName); this.multiCompileHostExt.invalidate(absFilePath); } this.support.write(fileName, content); @@ -143,7 +143,7 @@ export class NgtscTestEnvironment { if (this.multiCompileHostExt === null) { throw new Error(`Not caching files - call enableMultipleCompilations()`); } - const fullFile = path.join(this.support.basePath, fileName); + const fullFile = path.posix.join(this.support.basePath, fileName); this.multiCompileHostExt.invalidate(fullFile); } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 8dc8f76496..afde805218 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -18,13 +18,12 @@ const varRegExp = (name: string): RegExp => new RegExp(`var \\w+ = \\[\"${name}\ const viewQueryRegExp = (descend: boolean, ref?: string): RegExp => { const maybeRef = ref ? `${ref}` : `null`; - return new RegExp(`i0\\.\u0275\u0275viewQuery\\(\\w+, ${descend}, ${maybeRef}\\)`); + return new RegExp(`i0\\.ɵɵviewQuery\\(\\w+, ${descend}, ${maybeRef}\\)`); }; const contentQueryRegExp = (predicate: string, descend: boolean, ref?: string): RegExp => { const maybeRef = ref ? `${ref}` : `null`; - return new RegExp( - `i0\\.\u0275\u0275contentQuery\\(dirIndex, ${predicate}, ${descend}, ${maybeRef}\\)`); + return new RegExp(`i0\\.ɵɵcontentQuery\\(dirIndex, ${predicate}, ${descend}, ${maybeRef}\\)`); }; const setClassMetadataRegExp = (expectedType: string): RegExp => @@ -57,8 +56,8 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toContain('Service.ngInjectableDef ='); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); }); it('should compile Injectables with a generic service', () => { @@ -76,8 +75,7 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Store.ngInjectableDef ='); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef>;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef>;'); }); it('should compile Injectables with providedIn without errors', () => { @@ -101,12 +99,11 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toContain('Dep.ngInjectableDef ='); expect(jsContents).toContain('Service.ngInjectableDef ='); expect(jsContents) - .toContain( - 'return new (t || Service)(i0.\u0275\u0275inject(Dep)); }, providedIn: \'root\' });'); + .toContain('return new (t || Service)(i0.ɵɵinject(Dep)); }, providedIn: \'root\' });'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); }); it('should compile Injectables with providedIn and factory without errors', () => { @@ -131,7 +128,7 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); }); it('should compile Injectables with providedIn and factory with deps without errors', () => { @@ -154,14 +151,13 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Service.ngInjectableDef ='); expect(jsContents).toContain('factory: function Service_Factory(t) { var r = null; if (t) {'); - expect(jsContents).toContain('(r = new t(i0.\u0275\u0275inject(Dep)));'); + expect(jsContents).toContain('(r = new t(i0.ɵɵinject(Dep)));'); expect(jsContents) - .toContain( - '(r = (function (dep) { return new Service(dep); })(i0.\u0275\u0275inject(Dep)));'); + .toContain('(r = (function (dep) { return new Service(dep); })(i0.ɵɵinject(Dep)));'); expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ngInjectableDef: i0.\u0275\u0275InjectableDef;'); + expect(dtsContents).toContain('static ngInjectableDef: i0.ɵɵInjectableDef;'); }); it('should compile @Injectable with an @Optional dependency', () => { @@ -197,13 +193,13 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.\u0275\u0275defineComponent'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngComponentDef: i0.\u0275\u0275ComponentDefWithMeta'); + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); }); it('should compile Components (dynamic inline template) without errors', () => { @@ -221,13 +217,13 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.\u0275\u0275defineComponent'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngComponentDef: i0.\u0275\u0275ComponentDefWithMeta'); + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); }); it('should compile Components (function call inline template) without errors', () => { @@ -248,13 +244,13 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestCmp.ngComponentDef = i0.\u0275\u0275defineComponent'); + expect(jsContents).toContain('TestCmp.ngComponentDef = i0.ɵɵdefineComponent'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngComponentDef: i0.\u0275\u0275ComponentDefWithMeta'); + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); }); it('should compile Components (external template) without errors', () => { @@ -355,14 +351,13 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('TestBase.ngBaseDef = i0.\u0275\u0275defineBase'); - expect(jsContents).toContain('TestComponent.ngComponentDef = i0.\u0275\u0275defineComponent'); - expect(jsContents).toContain('TestDirective.ngDirectiveDef = i0.\u0275\u0275defineDirective'); - expect(jsContents).toContain('TestPipe.ngPipeDef = i0.\u0275\u0275definePipe'); - expect(jsContents) - .toContain('TestInjectable.ngInjectableDef = i0.\u0275\u0275defineInjectable'); - expect(jsContents).toContain('MyModule.ngModuleDef = i0.\u0275\u0275defineNgModule'); - expect(jsContents).toContain('MyModule.ngInjectorDef = i0.\u0275\u0275defineInjector'); + expect(jsContents).toContain('TestBase.ngBaseDef = i0.ɵɵdefineBase'); + expect(jsContents).toContain('TestComponent.ngComponentDef = i0.ɵɵdefineComponent'); + expect(jsContents).toContain('TestDirective.ngDirectiveDef = i0.ɵɵdefineDirective'); + expect(jsContents).toContain('TestPipe.ngPipeDef = i0.ɵɵdefinePipe'); + expect(jsContents).toContain('TestInjectable.ngInjectableDef = i0.ɵɵdefineInjectable'); + expect(jsContents).toContain('MyModule.ngModuleDef = i0.ɵɵdefineNgModule'); + expect(jsContents).toContain('MyModule.ngInjectorDef = i0.ɵɵdefineInjector'); expect(jsContents).toContain('inputs: { input: "input" }'); expect(jsContents).toContain('outputs: { output: "output" }'); }); @@ -448,43 +443,41 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) - .toContain('i0.\u0275\u0275defineNgModule({ type: TestModule, bootstrap: [TestCmp] });'); + .toContain('i0.ɵɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });'); + expect(jsContents) + .toContain('/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { declarations: [TestCmp] });'); expect(jsContents) .toContain( - '/*@__PURE__*/ i0.\u0275\u0275setNgModuleScope(TestModule, { declarations: [TestCmp] });'); - expect(jsContents) - .toContain( - 'i0.\u0275\u0275defineInjector({ factory: ' + + 'i0.ɵɵdefineInjector({ factory: ' + 'function TestModule_Factory(t) { return new (t || TestModule)(); } });'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngComponentDef: i0.\u0275\u0275ComponentDefWithMeta'); + 'static ngComponentDef: i0.ɵɵComponentDefWithMeta'); expect(dtsContents) .toContain( - 'static ngModuleDef: i0.\u0275\u0275NgModuleDefWithMeta'); + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); expect(dtsContents).not.toContain('__decorate'); }); - it('should not emit a \u0275\u0275setNgModuleScope call when no scope metadata is present', - () => { - env.tsconfig(); - env.write('test.ts', ` + it('should not emit a ɵɵsetNgModuleScope call when no scope metadata is present', () => { + env.tsconfig(); + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275\u0275defineNgModule({ type: TestModule });'); - expect(jsContents).not.toContain('\u0275\u0275setNgModuleScope(TestModule,'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); + expect(jsContents).not.toContain('ɵɵsetNgModuleScope(TestModule,'); + }); - it('should emit a \u0275registerNgModuleType call when the module has an id', () => { + it('should emit the id when the module\'s id is a string', () => { env.tsconfig(); env.write('test.ts', ` import {NgModule} from '@angular/core'; @@ -496,27 +489,26 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275registerNgModuleType(\'test\', TestModule);'); + expect(jsContents).toContain(`i0.ɵɵdefineNgModule({ type: TestModule, id: 'test' })`); }); - it('should emit a \u0275registerNgModuleType call when the module id is defined as `module.id`', - () => { - env.tsconfig(); - env.write('index.d.ts', ` + it('should emit the id when the module\'s id is defined as `module.id`', () => { + env.tsconfig(); + env.write('index.d.ts', ` declare const module = {id: string}; `); - env.write('test.ts', ` + env.write('test.ts', ` import {NgModule} from '@angular/core'; @NgModule({id: module.id}) export class TestModule {} `); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275registerNgModuleType(module.id, TestModule);'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule, id: module.id })'); + }); it('should filter out directives and pipes from module exports in the injector def', () => { env.tsconfig(); @@ -556,15 +548,15 @@ describe('ngtsc behavioral tests', () => { export class Comp {} `); env.write('node_modules/@angular/router/index.d.ts', ` - import {\u0275\u0275ComponentDefWithMeta, ModuleWithProviders, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵComponentDefWithMeta, ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class RouterComp { - static ngComponentDef: \u0275\u0275ComponentDefWithMeta + static ngComponentDef: ɵɵComponentDefWithMeta } declare class RouterModule { static forRoot(): ModuleWithProviders; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -573,7 +565,7 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) .toContain( - 'i0.\u0275\u0275defineInjector({ factory: function TestModule_Factory(t) ' + + 'i0.ɵɵdefineInjector({ factory: function TestModule_Factory(t) ' + '{ return new (t || TestModule)(); }, imports: [[OtherModule, RouterModule.forRoot()],' + '\n OtherModule,\n RouterModule] });'); }); @@ -605,18 +597,18 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275\u0275defineNgModule({ type: TestModule });'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); expect(jsContents) .toContain( - `TestModule.ngInjectorDef = i0.\u0275\u0275defineInjector({ factory: ` + + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + `Token, useValue: 'test' }], imports: [[OtherModule]] });`); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngModuleDef: i0.\u0275\u0275NgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.\u0275\u0275InjectorDef'); + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); }); it('should compile NgModules with factory providers without errors', () => { @@ -646,18 +638,18 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275\u0275defineNgModule({ type: TestModule });'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); expect(jsContents) .toContain( - `TestModule.ngInjectorDef = i0.\u0275\u0275defineInjector({ factory: ` + + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + `Token, useFactory: function () { return new Token(); } }], imports: [[OtherModule]] });`); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngModuleDef: i0.\u0275\u0275NgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.\u0275\u0275InjectorDef'); + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); }); it('should compile NgModules with factory providers and deps without errors', () => { @@ -691,18 +683,18 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275\u0275defineNgModule({ type: TestModule });'); + expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); expect(jsContents) .toContain( - `TestModule.ngInjectorDef = i0.\u0275\u0275defineInjector({ factory: ` + + `TestModule.ngInjectorDef = i0.ɵɵdefineInjector({ factory: ` + `function TestModule_Factory(t) { return new (t || TestModule)(); }, providers: [{ provide: ` + `Token, useFactory: function (dep) { return new Token(dep); }, deps: [Dep] }], imports: [[OtherModule]] });`); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'static ngModuleDef: i0.\u0275\u0275NgModuleDefWithMeta'); - expect(dtsContents).toContain('static ngInjectorDef: i0.\u0275\u0275InjectorDef'); + 'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta'); + expect(dtsContents).toContain('static ngInjectorDef: i0.ɵɵInjectorDef'); }); it('should compile NgModules with references to local components', () => { @@ -863,10 +855,9 @@ describe('ngtsc behavioral tests', () => { expect(jsContents) .toContain( - 'TestPipe.ngPipeDef = i0.\u0275\u0275definePipe({ name: "test-pipe", type: TestPipe, ' + + 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: false })'); - expect(dtsContents) - .toContain('static ngPipeDef: i0.\u0275\u0275PipeDefWithMeta;'); + expect(dtsContents).toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); }); it('should compile pure Pipes without errors', () => { @@ -887,10 +878,9 @@ describe('ngtsc behavioral tests', () => { expect(jsContents) .toContain( - 'TestPipe.ngPipeDef = i0.\u0275\u0275definePipe({ name: "test-pipe", type: TestPipe, ' + + 'TestPipe.ngPipeDef = i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + 'factory: function TestPipe_Factory(t) { return new (t || TestPipe)(); }, pure: true })'); - expect(dtsContents) - .toContain('static ngPipeDef: i0.\u0275\u0275PipeDefWithMeta;'); + expect(dtsContents).toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta;'); }); it('should compile Pipes with dependencies', () => { @@ -912,8 +902,7 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('return new (t || TestPipe)(i0.\u0275\u0275directiveInject(Dep));'); + expect(jsContents).toContain('return new (t || TestPipe)(i0.ɵɵdirectiveInject(Dep));'); }); it('should compile Pipes with generic types', () => { @@ -933,7 +922,7 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toContain('TestPipe.ngPipeDef ='); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) - .toContain('static ngPipeDef: i0.\u0275\u0275PipeDefWithMeta, "test-pipe">;'); + .toContain('static ngPipeDef: i0.ɵɵPipeDefWithMeta, "test-pipe">;'); }); it('should include @Pipes in @NgModule scopes', () => { @@ -959,7 +948,7 @@ describe('ngtsc behavioral tests', () => { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents) .toContain( - 'i0.\u0275\u0275NgModuleDefWithMeta'); + 'i0.ɵɵNgModuleDefWithMeta'); }); describe('empty and missing selectors', () => { @@ -1074,7 +1063,7 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain('ComponentDefWithMeta { `); env.driveMain(); - expect(env.getContents('test.js')).toContain('i0.\u0275\u0275pureFunction1'); + expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); }); it('should compile array literals inside function arguments', () => { @@ -1321,7 +1310,7 @@ describe('ngtsc behavioral tests', () => { `); env.driveMain(); - expect(env.getContents('test.js')).toContain('i0.\u0275\u0275pureFunction1'); + expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); }); }); @@ -1337,11 +1326,11 @@ describe('ngtsc behavioral tests', () => { `); env.write('node_modules/router/index.d.ts', ` - import {ModuleWithProviders, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; declare class RouterModule { static forRoot(): ModuleWithProviders; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -1354,7 +1343,7 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain(`import * as i1 from "router";`); expect(dtsContents) .toContain( - 'i0.\u0275\u0275NgModuleDefWithMeta'); + 'i0.ɵɵNgModuleDefWithMeta'); }); it('should extract the generic type if it is provided as qualified type name', () => { @@ -1379,9 +1368,9 @@ describe('ngtsc behavioral tests', () => { `); env.write('node_modules/router/internal.d.ts', ` - import {\u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class InternalRouterModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -1394,18 +1383,18 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain(`import * as i1 from "router";`); expect(dtsContents) .toContain( - 'i0.\u0275\u0275NgModuleDefWithMeta'); + 'i0.ɵɵNgModuleDefWithMeta'); }); it('should not reference a constant with a ModuleWithProviders value in ngModuleDef imports', () => { env.tsconfig(); env.write('dep.d.ts', ` - import {ModuleWithProviders, \u0275\u0275NgModuleDefWithMeta as \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ModuleWithProviders, ɵɵNgModuleDefWithMeta as ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class DepModule { static forRoot(arg1: any, arg2: any): ModuleWithProviders; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); env.write('test.ts', ` @@ -1440,13 +1429,13 @@ describe('ngtsc behavioral tests', () => { `); env.write('node_modules/router/index.d.ts', ` - import {ModuleWithProviders, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ModuleWithProviders, ɵɵNgModuleDefWithMeta} from '@angular/core'; export interface MyType extends ModuleWithProviders {} declare class RouterModule { static forRoot(): (MyType)&{ngModule:RouterModule}; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -1459,7 +1448,7 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain(`import * as i1 from "router";`); expect(dtsContents) .toContain( - 'i0.\u0275\u0275NgModuleDefWithMeta'); + 'i0.ɵɵNgModuleDefWithMeta'); }); it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', @@ -1479,7 +1468,7 @@ describe('ngtsc behavioral tests', () => { declare class RouterModule { static forRoot(): core.ModuleWithProviders; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -1492,7 +1481,7 @@ describe('ngtsc behavioral tests', () => { expect(dtsContents).toContain(`import * as i1 from "router";`); expect(dtsContents) .toContain( - 'i0.\u0275\u0275NgModuleDefWithMeta'); + 'i0.ɵɵNgModuleDefWithMeta'); }); it('should inject special types according to the metadata', () => { @@ -1530,7 +1519,7 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) .toContain( - `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.\u0275\u0275injectAttribute("test"), i0.\u0275\u0275directiveInject(i0.ChangeDetectorRef), i0.\u0275\u0275directiveInject(i0.ElementRef), i0.\u0275\u0275directiveInject(i0.Injector), i0.\u0275\u0275directiveInject(i0.Renderer2), i0.\u0275\u0275directiveInject(i0.TemplateRef), i0.\u0275\u0275directiveInject(i0.ViewContainerRef)); }`); + `factory: function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`); }); it('should generate queries for components', () => { @@ -1547,10 +1536,10 @@ describe('ngtsc behavioral tests', () => { } }) class FooCmp { - @ContentChild('bar', {read: TemplateRef}) child: any; + @ContentChild('bar', {read: TemplateRef, static: false}) child: any; @ContentChildren(TemplateRef) children: any; get aview(): any { return null; } - @ViewChild('accessor') set aview(value: any) {} + @ViewChild('accessor', {static: false}) set aview(value: any) {} } `); @@ -1560,9 +1549,9 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toMatch(varRegExp('test1')); expect(jsContents).toMatch(varRegExp('test2')); expect(jsContents).toMatch(varRegExp('accessor')); - // match `i0.\u0275\u0275contentQuery(dirIndex, _c1, true, TemplateRef)` + // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); - // match `i0.\u0275\u0275viewQuery(_c2, true, null)` + // match `i0.ɵɵviewQuery(_c2, true, null)` expect(jsContents).toMatch(viewQueryRegExp(true)); }); @@ -1579,7 +1568,7 @@ describe('ngtsc behavioral tests', () => { } }) class FooCmp { - @ContentChild('bar', {read: TemplateRef}) child: any; + @ContentChild('bar', {read: TemplateRef, static: false}) child: any; @ContentChildren(TemplateRef) children: any; get aview(): any { return null; } @ViewChild('accessor') set aview(value: any) {} @@ -1592,10 +1581,10 @@ describe('ngtsc behavioral tests', () => { expect(jsContents).toMatch(varRegExp('test1')); expect(jsContents).toMatch(varRegExp('test2')); expect(jsContents).toMatch(varRegExp('accessor')); - // match `i0.\u0275\u0275contentQuery(dirIndex, _c1, true, TemplateRef)` + // match `i0.ɵɵcontentQuery(dirIndex, _c1, true, TemplateRef)` expect(jsContents).toMatch(contentQueryRegExp('\\w+', true, 'TemplateRef')); - // match `i0.\u0275\u0275viewQuery(_c2, true, null)` + // match `i0.ɵɵviewQuery(_c2, true, null)` // Note that while ViewQuery doesn't necessarily make sense on a directive, because it doesn't // have a view, we still need to handle it because a component could extend the directive. expect(jsContents).toMatch(viewQueryRegExp(true)); @@ -1611,21 +1600,21 @@ describe('ngtsc behavioral tests', () => { template: '
', }) class FooCmp { - @ContentChild(forwardRef(() => TemplateRef)) child: any; + @ContentChild(forwardRef(() => TemplateRef), {static: false}) child: any; - @ContentChild(forwardRef(function() { return ViewContainerRef; })) child2: any; + @ContentChild(forwardRef(function() { return ViewContainerRef; }), {static: false}) child2: any; - @ContentChild((forwardRef((function() { return 'parens'; }) as any))) childInParens: any; + @ContentChild((forwardRef((function() { return 'parens'; }) as any)), {static: false}) childInParens: any; } `); env.driveMain(); const jsContents = env.getContents('test.js'); - // match `i0.\u0275\u0275contentQuery(dirIndex, TemplateRef, true, null)` + // match `i0.ɵɵcontentQuery(dirIndex, TemplateRef, true, null)` expect(jsContents).toMatch(contentQueryRegExp('TemplateRef', true)); - // match `i0.\u0275\u0275contentQuery(dirIndex, ViewContainerRef, true, null)` + // match `i0.ɵɵcontentQuery(dirIndex, ViewContainerRef, true, null)` expect(jsContents).toMatch(contentQueryRegExp('ViewContainerRef', true)); - // match `i0.\u0275\u0275contentQuery(dirIndex, _c0, true, null)` + // match `i0.ɵɵcontentQuery(dirIndex, _c0, true, null)` expect(jsContents).toContain('_c0 = ["parens"];'); expect(jsContents).toMatch(contentQueryRegExp('_c0', true)); }); @@ -1675,9 +1664,9 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275listener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); }); - i0.\u0275\u0275listener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.\u0275\u0275resolveDocument); - i0.\u0275\u0275listener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.\u0275\u0275resolveWindow); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); }); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵɵresolveDocument); + i0.ɵɵlistener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵɵresolveWindow); } } `; @@ -1773,17 +1762,17 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275allocHostVars(2); - i0.\u0275\u0275listener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); }); - i0.\u0275\u0275listener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.\u0275\u0275resolveBody); - i0.\u0275\u0275listener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); - i0.\u0275\u0275styling(_c0); + i0.ɵɵallocHostVars(2); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); }); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody); + i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); + i0.ɵɵstyling(_c0); } if (rf & 2) { - i0.\u0275\u0275elementAttribute(elIndex, "hello", i0.\u0275\u0275bind(ctx.foo)); - i0.\u0275\u0275property("prop", ctx.bar, null, true); - i0.\u0275\u0275classProp(0, ctx.someClass); - i0.\u0275\u0275stylingApply(); + i0.ɵɵattribute("hello", ctx.foo); + i0.ɵɵproperty("prop", ctx.bar, null, true); + i0.ɵɵclassProp(0, ctx.someClass); + i0.ɵɵstylingApply(); } } `; @@ -1812,7 +1801,7 @@ describe('ngtsc behavioral tests', () => { `); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.\u0275\u0275elementHostAttrs(["test", test])'); + expect(jsContents).toContain('i0.ɵɵelementHostAttrs(["test", test])'); }); it('should accept enum values as host bindings', () => { @@ -1837,7 +1826,7 @@ describe('ngtsc behavioral tests', () => { `); env.driveMain(); - expect(env.getContents('test.js')).toContain('"hello", i0.\u0275\u0275bind(ctx.foo)'); + expect(env.getContents('test.js')).toContain('i0.ɵɵattribute("hello", ctx.foo)'); }); it('should generate host listeners for directives within hostBindings section', () => { @@ -1859,7 +1848,7 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function Dir_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275listener("change", function Dir_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg); }); + i0.ɵɵlistener("change", function Dir_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg); }); } } `; @@ -1972,7 +1961,7 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('interpolation1("", ctx.text, "")'); + expect(jsContents).toContain('ɵɵtextInterpolate(ctx.text)'); }); it('should handle `encapsulation` field', () => { @@ -2054,7 +2043,7 @@ describe('ngtsc behavioral tests', () => { `); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('i0.\u0275\u0275elementProperty'); + expect(jsContents).not.toContain('i0.ɵɵelementProperty'); }); it('should correctly recognize local symbols', () => { @@ -2160,6 +2149,22 @@ describe('ngtsc behavioral tests', () => { expect(emptyFactory).toContain(`export var \u0275NonEmptyModule = true;`); }); + it('should generate correct type annotation for NgModuleFactory calls in ngfactories', () => { + env.tsconfig({'allowEmptyCodegenFiles': true}); + env.write('test.ts', ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '...', + }) + export class TestCmp {} + `); + env.driveMain(); + + const ngfactoryContents = env.getContents('test.ngfactory.d.ts'); + expect(ngfactoryContents).toContain(`i0.ɵNgModuleFactory`); + }); + it('should copy a top-level comment into a factory stub', () => { env.tsconfig({'allowEmptyCodegenFiles': true}); @@ -2309,12 +2314,10 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) - .toContain( - 'function Base_Factory(t) { return new (t || Base)(i0.\u0275\u0275inject(Dep)); }'); + .toContain('function Base_Factory(t) { return new (t || Base)(i0.ɵɵinject(Dep)); }'); + expect(jsContents).toContain('var \u0275Child_BaseFactory = i0.ɵɵgetInheritedFactory(Child)'); expect(jsContents) - .toContain('var \u0275Child_BaseFactory = i0.\u0275\u0275getInheritedFactory(Child)'); - expect(jsContents) - .toContain('function Child_Factory(t) { return \u0275Child_BaseFactory((t || Child)); }'); + .toContain('function Child_Factory(t) { return \u0275Child_BaseFactory(t || Child); }'); expect(jsContents) .toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }'); }); @@ -2337,8 +2340,7 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('var \u0275Dir_BaseFactory = i0.\u0275\u0275getInheritedFactory(Dir)'); + expect(jsContents).toContain('var \u0275Dir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)'); }); it('should wrap "directives" in component metadata in a closure when forward references are present', @@ -2479,8 +2481,8 @@ describe('ngtsc behavioral tests', () => { const jsContents = trim(env.getContents('test.js')); expect(jsContents).toContain(`import Default from './types';`); expect(jsContents).toContain(`import * as i1 from "./types";`); - expect(jsContents).toContain('i0.\u0275\u0275directiveInject(Default)'); - expect(jsContents).toContain('i0.\u0275\u0275directiveInject(i1.Other)'); + expect(jsContents).toContain('i0.ɵɵdirectiveInject(Default)'); + expect(jsContents).toContain('i0.ɵɵdirectiveInject(i1.Other)'); expect(jsContents).toMatch(setClassMetadataRegExp('type: Default')); expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Other')); }); @@ -2591,8 +2593,8 @@ describe('ngtsc behavioral tests', () => { const jsContents = env.getContents('test.js'); expect(jsContents) .toMatch( - /i\d\.\u0275\u0275setComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/); - expect(jsContents).not.toContain('/*__PURE__*/ i0.\u0275\u0275setComponentScope'); + /i\d\.ɵɵsetComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/); + expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵɵsetComponentScope'); }); it('should detect a cycle added entirely during compilation', () => { @@ -2783,14 +2785,14 @@ describe('ngtsc behavioral tests', () => { it('should not emit multiple references to the same directive', () => { env.tsconfig(); env.write('node_modules/external/index.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } export declare class ExternalModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); env.write('test.ts', ` @@ -2819,19 +2821,19 @@ describe('ngtsc behavioral tests', () => { it('should import directives by their external name', () => { env.tsconfig(); env.write('node_modules/external/index.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; import {InternalDir} from './internal'; export {InternalDir as ExternalDir} from './internal'; export declare class ExternalModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); env.write('node_modules/external/internal.d.ts', ` export declare class InternalDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } `); env.write('test.ts', ` @@ -3040,14 +3042,14 @@ describe('ngtsc behavioral tests', () => { export class Module {} `); env.write('node_modules/external/index.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } export declare class ExternalModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -3227,15 +3229,15 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275allocHostVars(6); + i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.\u0275\u0275elementAttribute(elIndex, "href", i0.\u0275\u0275bind(ctx.attrHref), i0.\u0275\u0275sanitizeUrlOrResourceUrl); - i0.\u0275\u0275elementAttribute(elIndex, "src", i0.\u0275\u0275bind(ctx.attrSrc), i0.\u0275\u0275sanitizeUrlOrResourceUrl); - i0.\u0275\u0275elementAttribute(elIndex, "action", i0.\u0275\u0275bind(ctx.attrAction), i0.\u0275\u0275sanitizeUrl); - i0.\u0275\u0275elementAttribute(elIndex, "profile", i0.\u0275\u0275bind(ctx.attrProfile), i0.\u0275\u0275sanitizeResourceUrl); - i0.\u0275\u0275elementAttribute(elIndex, "innerHTML", i0.\u0275\u0275bind(ctx.attrInnerHTML), i0.\u0275\u0275sanitizeHtml); - i0.\u0275\u0275elementAttribute(elIndex, "title", i0.\u0275\u0275bind(ctx.attrSafeTitle)); + i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl); + i0.ɵɵattribute("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl); + i0.ɵɵattribute("action", ctx.attrAction, i0.ɵɵsanitizeUrl); + i0.ɵɵattribute("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl); + i0.ɵɵattribute("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml); + i0.ɵɵattribute("title", ctx.attrSafeTitle); } } `; @@ -3282,15 +3284,15 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275allocHostVars(6); + i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.\u0275\u0275property("href", ctx.propHref, i0.\u0275\u0275sanitizeUrlOrResourceUrl, true); - i0.\u0275\u0275property("src", ctx.propSrc, i0.\u0275\u0275sanitizeUrlOrResourceUrl, true); - i0.\u0275\u0275property("action", ctx.propAction, i0.\u0275\u0275sanitizeUrl, true); - i0.\u0275\u0275property("profile", ctx.propProfile, i0.\u0275\u0275sanitizeResourceUrl, true); - i0.\u0275\u0275property("innerHTML", ctx.propInnerHTML, i0.\u0275\u0275sanitizeHtml, true); - i0.\u0275\u0275property("title", ctx.propSafeTitle, null, true); + i0.ɵɵproperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl, true); + i0.ɵɵproperty("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl, true); + i0.ɵɵproperty("action", ctx.propAction, i0.ɵɵsanitizeUrl, true); + i0.ɵɵproperty("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl, true); + i0.ɵɵproperty("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml, true); + i0.ɵɵproperty("title", ctx.propSafeTitle, null, true); } } `; @@ -3322,15 +3324,15 @@ describe('ngtsc behavioral tests', () => { const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.\u0275\u0275allocHostVars(6); + i0.ɵɵallocHostVars(6); } if (rf & 2) { - i0.\u0275\u0275property("src", ctx.srcProp, null, true); - i0.\u0275\u0275property("href", ctx.hrefProp, null, true); - i0.\u0275\u0275property("title", ctx.titleProp, null, true); - i0.\u0275\u0275elementAttribute(elIndex, "src", i0.\u0275\u0275bind(ctx.srcAttr)); - i0.\u0275\u0275elementAttribute(elIndex, "href", i0.\u0275\u0275bind(ctx.hrefAttr)); - i0.\u0275\u0275elementAttribute(elIndex, "title", i0.\u0275\u0275bind(ctx.titleAttr)); + i0.ɵɵproperty("src", ctx.srcProp, null, true); + i0.ɵɵproperty("href", ctx.hrefProp, null, true); + i0.ɵɵproperty("title", ctx.titleProp, null, true); + i0.ɵɵattribute("src", ctx.srcAttr); + i0.ɵɵattribute("href", ctx.hrefAttr); + i0.ɵɵattribute("title", ctx.titleAttr); } } `; @@ -3360,13 +3362,13 @@ describe('ngtsc behavioral tests', () => { beforeEach(() => { env.tsconfig(); env.write('node_modules/@angular/router/index.d.ts', ` - import {ModuleWithProviders, \u0275\u0275NgModuleDefWithMeta as \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ModuleWithProviders, ɵɵNgModuleDefWithMeta as ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare var ROUTES; export declare class RouterModule { static forRoot(arg1: any, arg2: any): ModuleWithProviders; static forChild(arg1: any): ModuleWithProviders; - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); }); @@ -3996,24 +3998,24 @@ export const Foo = Foo__PRE_R3__; // 'alpha' declares the directive which will ultimately be imported. env.write('alpha.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class ExternalDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } export declare class AlphaModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); // 'beta' re-exports AlphaModule from alpha. env.write('beta.d.ts', ` - import {\u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵNgModuleDefWithMeta} from '@angular/core'; import {AlphaModule} from './alpha'; export declare class BetaModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); @@ -4045,26 +4047,26 @@ export const Foo = Foo__PRE_R3__; it('should write alias ES2015 exports for NgModule exported directives', () => { env.tsconfig({'_useHostForImportGeneration': true}); env.write('external.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; import {LibModule} from './lib'; export declare class ExternalDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } export declare class ExternalModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); env.write('lib.d.ts', ` - import {\u0275\u0275DirectiveDefWithMeta, \u0275\u0275NgModuleDefWithMeta} from '@angular/core'; + import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core'; export declare class LibDir { - static ngDirectiveDef: \u0275\u0275DirectiveDefWithMeta; + static ngDirectiveDef: ɵɵDirectiveDefWithMeta; } export declare class LibModule { - static ngModuleDef: \u0275\u0275NgModuleDefWithMeta; + static ngModuleDef: ɵɵNgModuleDefWithMeta; } `); env.write('foo.ts', ` diff --git a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts index ed1d6cee85..4b6bbdc537 100644 --- a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts @@ -44,7 +44,7 @@ describe('template source-mapping', () => { {source: '

', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'}); expect(mappings).toContain({ source: 'Hello {{ name }}', - generated: 'i0.ɵɵtextBinding(1, i0.ɵɵinterpolation1("Hello ", ctx.name, ""))', + generated: 'i0.ɵɵtextInterpolate1("Hello ", ctx.name, "")', sourceUrl: '../test.ts' }); expect(mappings).toContain( @@ -57,8 +57,7 @@ describe('template source-mapping', () => { {source: '

', generated: 'i0.ɵɵelementStart(0, "h2")', sourceUrl: '../test.ts'}); expect(mappings).toContain({ source: '{{ greeting + " " + name }}', - generated: - 'i0.ɵɵtextBinding(1, i0.ɵɵinterpolation1("", ((ctx.greeting + " ") + ctx.name), ""))', + generated: 'i0.ɵɵtextInterpolate(ctx.greeting + " " + ctx.name)', sourceUrl: '../test.ts' }); expect(mappings).toContain( @@ -85,8 +84,7 @@ describe('template source-mapping', () => { {source: '
', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'}); expect(mappings).toContain({ source: '{{200.3 | percent : 2 }}', - generated: - 'i0.ɵɵtextBinding(1, i0.ɵɵinterpolation1("", i0.ɵɵpipeBind2(2, 1, 200.3, 2), ""))', + generated: 'i0.ɵɵtextInterpolate(i0.ɵɵpipeBind2(2, 1, 200.3, 2))', sourceUrl: '../test.ts' }); expect(mappings).toContain( @@ -119,7 +117,7 @@ describe('template source-mapping', () => { }); expect(mappings).toContain({ source: '[attr]="greeting + name"', - generated: 'i0.ɵɵproperty("attr", (ctx.greeting + ctx.name))', + generated: 'i0.ɵɵproperty("attr", ctx.greeting + ctx.name)', sourceUrl: '../test.ts' }); }); @@ -164,12 +162,12 @@ describe('template source-mapping', () => { expect(mappings).toContain( {source: 'Add Item', generated: 'i0.ɵɵtext(1, "Add Item")', sourceUrl: '../test.ts'}); expect(mappings).toContain( - {source: 'items.push(', generated: 'ctx.items.push((', sourceUrl: '../test.ts'}); + {source: 'items.push(', generated: 'ctx.items.push(', sourceUrl: '../test.ts'}); expect(mappings).toContain( {source: `'item' `, generated: `"item"`, sourceUrl: '../test.ts'}); expect(mappings).toContain({ source: '+ items.length)', - generated: ' + ctx.items.length))', + generated: ' + ctx.items.length)', sourceUrl: '../test.ts' }); expect(mappings).toContain( @@ -270,7 +268,7 @@ describe('template source-mapping', () => { // expect(mappings).toContain({ // source: '{{ name }}', - // generated: 'i0.ɵɵtextBinding(1, i0.ɵɵinterpolation1("", ctx_r0.name, ""))', + // generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)', // sourceUrl: '../test.ts' // }); }); @@ -294,7 +292,7 @@ describe('template source-mapping', () => { // expect(mappings).toContain({ // source: '{{ name }}', - // generated: 'i0.ɵɵtextBinding(1, i0.ɵɵinterpolation1("", ctx_r0.name, ""))', + // generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)', // sourceUrl: '../test.ts' // }); }); @@ -334,7 +332,7 @@ describe('template source-mapping', () => { {source: '

', generated: 'i0.ɵɵelementStart(0, "h3")', sourceUrl: '../test.ts'}); expect(mappings).toContain({ source: '', - generated: 'i0.ɵɵprojection(1, 1)', + generated: 'i0.ɵɵprojection(1)', sourceUrl: '../test.ts' }); expect(mappings).toContain( @@ -342,7 +340,7 @@ describe('template source-mapping', () => { expect(mappings).toContain( {source: '
', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'}); expect(mappings).toContain( - {source: '', generated: 'i0.ɵɵprojection(3)', sourceUrl: '../test.ts'}); + {source: '', generated: 'i0.ɵɵprojection(3, 1)', sourceUrl: '../test.ts'}); expect(mappings).toContain( {source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'}); }); @@ -370,7 +368,7 @@ describe('template source-mapping', () => { // Update mode expect(mappings).toContain({ - generated: 'i0.ɵɵtextBinding(3, i0.ɵɵinterpolation1("", (1 + 2), ""))', + generated: 'i0.ɵɵtextInterpolate(1 + 2)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts' }); @@ -396,9 +394,10 @@ describe('template source-mapping', () => { expect(mappings).toContain( {generated: 'i0.ɵɵelementEnd()', source: '

', sourceUrl: '../test.ts'}); + // TODO(benlesh): We need to circle back and prevent the extra parens from being generated. // Update mode expect(mappings).toContain({ - generated: 'i0.ɵɵtextBinding(3, i0.ɵɵinterpolation1("", (1 + 2), ""))', + generated: 'i0.ɵɵtextInterpolate(1 + 2)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts' }); @@ -452,7 +451,7 @@ describe('template source-mapping', () => { // Update mode expect(mappings).toContain({ - generated: 'i0.ɵɵtextBinding(3, i0.ɵɵinterpolation1("", (1 + 2), ""))', + generated: 'i0.ɵɵtextInterpolate(1 + 2)', source: '{{ 1 + 2 }}', sourceUrl: '../dir/test.html' }); @@ -496,7 +495,7 @@ describe('template source-mapping', () => { // Update mode expect(mappings).toContain({ - generated: 'i0.ɵɵtextBinding(3, i0.ɵɵinterpolation1("", (1 + 2), ""))', + generated: 'i0.ɵɵtextInterpolate(1 + 2)', source: '{{ 1 + 2 }}', sourceUrl: '../extraRootDir/test.html' }); diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index e4f86e318d..2ed017e3e9 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -39,7 +39,7 @@ export declare class NgForOf { export declare class NgIf { ngIf: any; - static ngTemplateGuard_ngIf(dir: NgIf, expr: E): expr is NonNullable + static ngTemplateGuard_ngIf: 'binding'; static ngDirectiveDef: i0.ɵɵDirectiveDefWithMeta, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>; } @@ -100,6 +100,29 @@ describe('ngtsc type checking', () => { env.driveMain(); }); + it('should check usage of NgIf with explicit non-null guard', () => { + env.write('test.ts', ` + import {CommonModule} from '@angular/common'; + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
{{user.name}}
', + }) + class TestCmp { + user: {name: string}|null; + } + + @NgModule({ + declarations: [TestCmp], + imports: [CommonModule], + }) + class Module {} + `); + + env.driveMain(); + }); + it('should check basic usage of NgFor', () => { env.write('test.ts', ` import {CommonModule} from '@angular/common'; @@ -247,6 +270,31 @@ describe('ngtsc type checking', () => { expect(diags[0].messageText).toContain('does_not_exist'); }); + it('should property type-check a microsyntax variable with the same name as the expression', + () => { + env.write('test.ts', ` + import {CommonModule} from '@angular/common'; + import {Component, Input, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
{{foo}}
', + }) + export class TestCmp { + foo: any; + } + + @NgModule({ + declarations: [TestCmp], + imports: [CommonModule], + }) + export class Module {} + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + it('should properly type-check inherited directives', () => { env.write('test.ts', ` import {Component, Directive, Input, NgModule} from '@angular/core'; diff --git a/packages/compiler-cli/test/test_support.ts b/packages/compiler-cli/test/test_support.ts index 13a883eaf3..4e77479630 100644 --- a/packages/compiler-cli/test/test_support.ts +++ b/packages/compiler-cli/test/test_support.ts @@ -19,7 +19,7 @@ export function makeTempDir(): string { let dir: string; while (true) { const id = (Math.random() * 1000000).toFixed(0); - dir = path.join(tmpdir, `tmp.${id}`); + dir = path.posix.join(tmpdir, `tmp.${id}`); if (!fs.existsSync(dir)) break; } fs.mkdirSync(dir); diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 7214522ddd..8fb8a44d07 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -106,6 +106,7 @@ export interface R3NgModuleMetadataFacade { exports: Function[]; emitInline: boolean; schemas: {name: string}[]|null; + id: string|null; } export interface R3InjectorMetadataFacade { diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index b98b0090ba..67cc3ceeb8 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -582,7 +582,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor { // / \ / \ // . c . e // / \ / \ - // a b , d + // a b . d // / \ // . c // / \ diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index b3424ddfbf..f228d6ead0 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -29,7 +29,7 @@ export interface Query { read: any; isViewQuery: boolean; selector: any; - static?: boolean; + static: boolean; } export const createContentChildren = makeMetadataFactory( @@ -491,5 +491,21 @@ export const enum AttributeMarker { * ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']] * ``` */ - ProjectAs = 5 + ProjectAs = 5, + + /** + * Signals that the following attribute will be translated by runtime i18n + * + * For example, given the following HTML: + * + * ``` + *
+ * ``` + * + * the generated code is: + * + * ``` + * var _c1 = ['moo', 'car', AttributeMarker.I18n, 'foo', 'bar']; + */ + I18n = 6, } diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 4e135ad90a..c14a4680bf 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -91,6 +91,7 @@ export class CompilerFacadeImpl implements CompilerFacade { emitInline: true, containsForwardDecls: false, schemas: facade.schemas ? facade.schemas.map(wrapReference) : null, + id: facade.id ? new WrappedNodeExpr(facade.id) : null, }; const res = compileNgModule(meta); return this.jitExpression(res.expression, angularCoreEnv, sourceMapUrl, []); diff --git a/packages/compiler/src/output/output_jit.ts b/packages/compiler/src/output/output_jit.ts index 1d3d2670b0..e446a1351b 100644 --- a/packages/compiler/src/output/output_jit.ts +++ b/packages/compiler/src/output/output_jit.ts @@ -56,7 +56,7 @@ export class JitEvaluator { evaluateCode( sourceUrl: string, ctx: EmitterVisitorContext, vars: {[key: string]: any}, createSourceMap: boolean): any { - let fnBody = `${ctx.toSource()}\n//# sourceURL=${sourceUrl}`; + let fnBody = `"use strict";${ctx.toSource()}\n//# sourceURL=${sourceUrl}`; const fnArgNames: string[] = []; const fnArgValues: any[] = []; for (const argName in vars) { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 7e872d9a3e..285bb4c7b0 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -33,14 +33,35 @@ export class Identifiers { static select: o.ExternalReference = {name: 'ɵɵselect', moduleName: CORE}; - static componentHostSyntheticProperty: - o.ExternalReference = {name: 'ɵɵcomponentHostSyntheticProperty', moduleName: CORE}; + static updateSyntheticHostBinding: + o.ExternalReference = {name: 'ɵɵupdateSyntheticHostBinding', moduleName: CORE}; static componentHostSyntheticListener: o.ExternalReference = {name: 'ɵɵcomponentHostSyntheticListener', moduleName: CORE}; static elementAttribute: o.ExternalReference = {name: 'ɵɵelementAttribute', moduleName: CORE}; + static attribute: o.ExternalReference = {name: 'ɵɵattribute', moduleName: CORE}; + + static attributeInterpolate1: + o.ExternalReference = {name: 'ɵɵattributeInterpolate1', moduleName: CORE}; + static attributeInterpolate2: + o.ExternalReference = {name: 'ɵɵattributeInterpolate2', moduleName: CORE}; + static attributeInterpolate3: + o.ExternalReference = {name: 'ɵɵattributeInterpolate3', moduleName: CORE}; + static attributeInterpolate4: + o.ExternalReference = {name: 'ɵɵattributeInterpolate4', moduleName: CORE}; + static attributeInterpolate5: + o.ExternalReference = {name: 'ɵɵattributeInterpolate5', moduleName: CORE}; + static attributeInterpolate6: + o.ExternalReference = {name: 'ɵɵattributeInterpolate6', moduleName: CORE}; + static attributeInterpolate7: + o.ExternalReference = {name: 'ɵɵattributeInterpolate7', moduleName: CORE}; + static attributeInterpolate8: + o.ExternalReference = {name: 'ɵɵattributeInterpolate8', moduleName: CORE}; + static attributeInterpolateV: + o.ExternalReference = {name: 'ɵɵattributeInterpolateV', moduleName: CORE}; + static classProp: o.ExternalReference = {name: 'ɵɵclassProp', moduleName: CORE}; static elementContainerStart: @@ -59,6 +80,8 @@ export class Identifiers { static stylingApply: o.ExternalReference = {name: 'ɵɵstylingApply', moduleName: CORE}; + static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE}; + static elementHostAttrs: o.ExternalReference = {name: 'ɵɵelementHostAttrs', moduleName: CORE}; static containerCreate: o.ExternalReference = {name: 'ɵɵcontainer', moduleName: CORE}; @@ -81,6 +104,17 @@ export class Identifiers { static getCurrentView: o.ExternalReference = {name: 'ɵɵgetCurrentView', moduleName: CORE}; + static textInterpolate: o.ExternalReference = {name: 'ɵɵtextInterpolate', moduleName: CORE}; + static textInterpolate1: o.ExternalReference = {name: 'ɵɵtextInterpolate1', moduleName: CORE}; + static textInterpolate2: o.ExternalReference = {name: 'ɵɵtextInterpolate2', moduleName: CORE}; + static textInterpolate3: o.ExternalReference = {name: 'ɵɵtextInterpolate3', moduleName: CORE}; + static textInterpolate4: o.ExternalReference = {name: 'ɵɵtextInterpolate4', moduleName: CORE}; + static textInterpolate5: o.ExternalReference = {name: 'ɵɵtextInterpolate5', moduleName: CORE}; + static textInterpolate6: o.ExternalReference = {name: 'ɵɵtextInterpolate6', moduleName: CORE}; + static textInterpolate7: o.ExternalReference = {name: 'ɵɵtextInterpolate7', moduleName: CORE}; + static textInterpolate8: o.ExternalReference = {name: 'ɵɵtextInterpolate8', moduleName: CORE}; + static textInterpolateV: o.ExternalReference = {name: 'ɵɵtextInterpolateV', moduleName: CORE}; + static restoreView: o.ExternalReference = {name: 'ɵɵrestoreView', moduleName: CORE}; static interpolation1: o.ExternalReference = {name: 'ɵɵinterpolation1', moduleName: CORE}; @@ -239,9 +273,6 @@ export class Identifiers { moduleName: CORE, }; - static registerNgModuleType: - o.ExternalReference = {name: 'ɵregisterNgModuleType', moduleName: CORE}; - // sanitization-related functions static sanitizeHtml: o.ExternalReference = {name: 'ɵɵsanitizeHtml', moduleName: CORE}; static sanitizeStyle: o.ExternalReference = {name: 'ɵɵsanitizeStyle', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts index 213e762c5f..df83a468d6 100644 --- a/packages/compiler/src/render3/r3_module_compiler.ts +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -67,6 +67,9 @@ export interface R3NgModuleMetadata { * The set of schemas that declare elements to be allowed in the NgModule. */ schemas: R3Reference[]|null; + + /** Unique ID or expression representing the unique ID of an NgModule. */ + id: o.Expression|null; } /** @@ -81,7 +84,8 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { exports, schemas, containsForwardDecls, - emitInline + emitInline, + id } = meta; const additionalStatements: o.Statement[] = []; @@ -93,7 +97,8 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { declarations: o.Expression, imports: o.Expression, exports: o.Expression, - schemas: o.LiteralArrayExpr + schemas: o.LiteralArrayExpr, + id: o.Expression }; // Only generate the keys in the metadata if the arrays have values. @@ -130,6 +135,10 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { definitionMap.schemas = o.literalArr(schemas.map(ref => ref.value)); } + if (id) { + definitionMap.id = id; + } + const expression = o.importExpr(R3.defineNgModule).callFn([mapToMapExpression(definitionMap)]); const type = new o.ExpressionType(o.importExpr(R3.NgModuleDefWithMeta, [ new o.ExpressionType(moduleType), tupleTypeOf(declarations), tupleTypeOf(imports), diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 92dcd4cf53..5d6378730d 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -13,7 +13,6 @@ import {BindingForm, convertPropertyBinding} from '../../compiler_util/expressio import {ConstantPool, DefinitionKind} from '../../constant_pool'; import * as core from '../../core'; import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast'; -import {LifecycleHooks} from '../../lifecycle_reflector'; import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config'; import * as o from '../../output/output_ast'; import {ParseError, ParseSourceSpan, typeSourceSpan} from '../../parse_util'; @@ -668,18 +667,7 @@ function createHostBindingsFunction( sanitizerFn = resolveSanitizationFn(securityContexts[0], isAttribute); } } - - const isPropertyInstruction = instruction === R3.property; - const instructionParams: o.Expression[] = isPropertyInstruction ? - [ - o.literal(bindingName), - bindingExpr.currValExpr, - ] : - [ - elVarExp, - o.literal(bindingName), - o.importExpr(R3.bind).callFn([bindingExpr.currValExpr]), - ]; + const instructionParams = [o.literal(bindingName), bindingExpr.currValExpr]; if (sanitizerFn) { instructionParams.push(sanitizerFn); } @@ -725,6 +713,7 @@ function createHostBindingsFunction( // the update block of a component/directive templateFn/hostBindingsFn so that the bindings // are evaluated and updated for the element. styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => { + totalHostVarsCount += instruction.allocateBindingSlots; updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn)); }); } @@ -776,14 +765,14 @@ function getBindingNameAndInstruction(binding: ParsedProperty): const attrMatches = bindingName.match(ATTR_REGEX); if (attrMatches) { bindingName = attrMatches[1]; - instruction = R3.elementAttribute; + instruction = R3.attribute; } else { if (binding.isAnimation) { bindingName = prepareSyntheticPropertyName(bindingName); // host bindings that have a synthetic property (e.g. @foo) should always be rendered // in the context of the component and not the parent. Therefore there is a special // compatibility instruction available for this purpose. - instruction = R3.componentHostSyntheticProperty; + instruction = R3.updateSyntheticHostBinding; } else { instruction = R3.property; } diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 3bbaf8640e..3ca675ecaf 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -15,6 +15,7 @@ import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; import {parse as parseStyle} from './style_parser'; +import {compilerIsNewStylingInUse} from './styling_state'; import {ValueConverter} from './template'; const IMPORTANT_FLAG = '!important'; @@ -88,6 +89,7 @@ export class StylingBuilder { /** an array of each [class.name] input */ private _singleClassInputs: BoundStylingEntry[]|null = null; private _lastStylingInput: BoundStylingEntry|null = null; + private _firstStylingInput: BoundStylingEntry|null = null; // maps are used instead of hash maps because a Map will // retain the ordering of the keys @@ -180,6 +182,7 @@ export class StylingBuilder { registerIntoMap(this._stylesIndex, property); } this._lastStylingInput = entry; + this._firstStylingInput = this._firstStylingInput || entry; this.hasBindings = true; return entry; } @@ -199,6 +202,7 @@ export class StylingBuilder { registerIntoMap(this._classesIndex, property); } this._lastStylingInput = entry; + this._firstStylingInput = this._firstStylingInput || entry; this.hasBindings = true; return entry; } @@ -363,6 +367,11 @@ export class StylingBuilder { private _buildMapBasedInstruction( valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry) { let totalBindingSlotsRequired = 0; + if (compilerIsNewStylingInUse()) { + // the old implementation does not reserve slot values for + // binding entries. The new one does. + totalBindingSlotsRequired++; + } // these values must be outside of the update block so that they can // be evaluated (the AST visit call) during creation time so that any @@ -389,6 +398,11 @@ export class StylingBuilder { const bindingIndex: number = mapIndex.get(input.name !) !; const value = input.value.visit(valueConverter); totalBindingSlotsRequired += (value instanceof Interpolation) ? value.expressions.length : 0; + if (compilerIsNewStylingInUse()) { + // the old implementation does not reserve slot values for + // binding entries. The new one does. + totalBindingSlotsRequired++; + } return { sourceSpan: input.sourceSpan, allocateBindingSlots: totalBindingSlotsRequired, reference, @@ -442,6 +456,15 @@ export class StylingBuilder { }; } + private _buildSanitizerFn() { + return { + sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null, + reference: R3.styleSanitizer, + allocateBindingSlots: 0, + buildParams: () => [o.importExpr(R3.defaultStyleSanitizer)] + }; + } + /** * Constructs all instructions which contain the expressions that will be placed * into the update block of a template function or a directive hostBindings function. @@ -449,6 +472,9 @@ export class StylingBuilder { buildUpdateLevelInstructions(valueConverter: ValueConverter) { const instructions: Instruction[] = []; if (this.hasBindings) { + if (compilerIsNewStylingInUse() && this._useDefaultSanitizer) { + instructions.push(this._buildSanitizerFn()); + } const styleMapInstruction = this.buildStyleMapInstruction(valueConverter); if (styleMapInstruction) { instructions.push(styleMapInstruction); @@ -477,7 +503,7 @@ function isStyleSanitizable(prop: string): boolean { return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || prop === 'border-image' || prop === 'borderImage' || prop === 'filter' || prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' || - prop === 'listStyleImage'; + prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath'; } /** diff --git a/packages/compiler/src/render3/view/styling_state.ts b/packages/compiler/src/render3/view/styling_state.ts new file mode 100644 index 0000000000..c63ab6d8e6 --- /dev/null +++ b/packages/compiler/src/render3/view/styling_state.ts @@ -0,0 +1,37 @@ +/** +* @license +* Copyright Google Inc. 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 +*/ + +/** + * A temporary enum of states that inform the core whether or not + * to defer all styling instruction calls to the old or new + * styling implementation. + */ +export const enum CompilerStylingMode { + UseOld = 0, + UseBothOldAndNew = 1, + UseNew = 2, +} + +let _stylingMode = 0; + +/** + * Temporary function used to inform the existing styling algorithm + * code to delegate all styling instruction calls to the new refactored + * styling code. + */ +export function compilerSetStylingMode(mode: CompilerStylingMode) { + _stylingMode = mode; +} + +export function compilerIsNewStylingInUse() { + return _stylingMode > CompilerStylingMode.UseOld; +} + +export function compilerAllowOldStyling() { + return _stylingMode < CompilerStylingMode.UseNew; +} diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index d67b353551..a1eb09df4b 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -372,12 +372,8 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor { private ingest(template: Template|Node[]): void { if (template instanceof Template) { - // For s, process inputs, outputs, template attributes, - // variables, and child nodes. - // References were processed in the scope of the containing template. - template.inputs.forEach(this.visitNode); - template.outputs.forEach(this.visitNode); - template.templateAttrs.forEach(this.visitNode); + // For s, process only variables and child nodes. Inputs, outputs, templateAttrs, + // and references were all processed in the scope of the containing template. template.variables.forEach(this.visitNode); template.children.forEach(this.visitNode); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 758ceb66ea..552dfbd675 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -40,9 +40,6 @@ import {Instruction, StylingBuilder} from './styling_builder'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; -// Default selector used by `` if none specified -const DEFAULT_NG_CONTENT_SELECTOR = '*'; - // Selector attribute name of `` const NG_CONTENT_SELECT_ATTR = 'select'; @@ -146,14 +143,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private fileBasedI18nSuffix: string; - // Whether the template includes tags. - private _hasNgContent: boolean = false; - - // Selectors found in the tags in the template. - private _ngContentSelectors: string[] = []; + // Projection slots found in the template. Projection slots can distribute projected + // nodes based on a selector, or can just use the wildcard selector to match + // all nodes which aren't matching any selector. + private _ngContentReservedSlots: (string|'*')[] = []; // Number of non-default selectors found in all parent templates of this template. We need to - // track it to properly adjust projection bucket index in the `projection` instruction. + // track it to properly adjust projection slot index in the `projection` instruction. private _ngContentSelectorsOffset = 0; constructor( @@ -247,16 +243,19 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // instructions can be generated with the correct internal const count. this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn()); - // Output the `projectionDef` instruction when some `` are present. - // The `projectionDef` instruction only emitted for the component template and it is skipped for - // nested templates ( tags). - if (this.level === 0 && this._hasNgContent) { + // Output the `projectionDef` instruction when some `` tags are present. + // The `projectionDef` instruction is only emitted for the component template and + // is skipped for nested templates ( tags). + if (this.level === 0 && this._ngContentReservedSlots.length) { const parameters: o.Expression[] = []; - // Only selectors with a non-default value are generated - if (this._ngContentSelectors.length) { - const r3Selectors = this._ngContentSelectors.map(s => core.parseSelectorToR3Selector(s)); - parameters.push(this.constantPool.getConstLiteral(asLiteral(r3Selectors), true)); + // By default the `projectionDef` instructions creates one slot for the wildcard + // selector if no parameters are passed. Therefore we only want to allocate a new + // array for the projection slots if the default projection slot is not sufficient. + if (this._ngContentReservedSlots.length > 1 || this._ngContentReservedSlots[0] !== '*') { + const r3ReservedSlots = this._ngContentReservedSlots.map( + s => s !== '*' ? core.parseSelectorToR3Selector(s) : s); + parameters.push(this.constantPool.getConstLiteral(asLiteral(r3ReservedSlots), true)); } // Since we accumulate ngContent selectors while processing template elements, @@ -461,14 +460,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } visitContent(ngContent: t.Content) { - this._hasNgContent = true; const slot = this.allocateDataSlot(); - let selectorIndex = ngContent.selector === DEFAULT_NG_CONTENT_SELECTOR ? - 0 : - this._ngContentSelectors.push(ngContent.selector) + this._ngContentSelectorsOffset; + const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length; const parameters: o.Expression[] = [o.literal(slot)]; const attributes: o.Expression[] = []; + this._ngContentReservedSlots.push(ngContent.selector); + ngContent.attributes.forEach((attribute) => { const {name, value} = attribute; if (name === NG_PROJECT_AS_ATTR_NAME) { @@ -479,9 +477,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); if (attributes.length > 0) { - parameters.push(o.literal(selectorIndex), o.literalArr(attributes)); - } else if (selectorIndex !== 0) { - parameters.push(o.literal(selectorIndex)); + parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes)); + } else if (projectionSlotIdx !== 0) { + parameters.push(o.literal(projectionSlotIdx)); } this.creationInstruction(ngContent.sourceSpan, R3.projection, parameters); @@ -538,8 +536,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart` // arguments i18nAttrs.push(attr); + } else { + outputAttrs.push(attr); } - outputAttrs.push(attr); } } @@ -565,8 +564,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart` // arguments i18nAttrs.push(input); + } else { + allOtherInputs.push(input); } - allOtherInputs.push(input); } }); @@ -579,7 +579,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); // add attributes for directive and projection matching purposes - attributes.push(...this.prepareNonRenderAttrs(allOtherInputs, element.outputs, stylingBuilder)); + attributes.push(...this.prepareNonRenderAttrs( + allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs)); parameters.push(this.toAttrsParam(attributes)); // local refs (ex.:
) @@ -643,8 +644,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params)); converted.expressions.forEach(expression => { hasBindings = true; - const binding = this.convertExpressionBinding(implicit, expression); - this.updateInstruction(elementIndex, element.sourceSpan, R3.i18nExp, [binding]); + this.updateInstruction( + elementIndex, element.sourceSpan, R3.i18nExp, + () => [this.convertExpressionBinding(implicit, expression)]); }); } } @@ -752,31 +754,30 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (inputType === BindingType.Property) { if (value instanceof Interpolation) { - this.updateInstruction( - elementIndex, input.sourceSpan, getPropertyInterpolationExpression(value), - () => - [o.literal(attrName), - ...this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value), - ...params]); - + // prop="{{value}}" and friends + this.interpolatedUpdateInstruction( + getPropertyInterpolationExpression(value), elementIndex, attrName, input, value, + params); } else { - // Bound, un-interpolated properties - this.updateInstruction(elementIndex, input.sourceSpan, R3.property, () => { - return [ - o.literal(attrName), this.convertPropertyBinding(implicit, value, true), ...params - ]; - }); + // [prop]="value" + this.boundUpdateInstruction( + R3.property, elementIndex, attrName, input, implicit, value, params); + } + } else if (inputType === BindingType.Attribute) { + if (value instanceof Interpolation && getInterpolationArgsLength(value) > 1) { + // attr.name="text{{value}}" and friends + this.interpolatedUpdateInstruction( + getAttributeInterpolationExpression(value), elementIndex, attrName, input, value, + params); + } else { + const boundValue = value instanceof Interpolation ? value.expressions[0] : value; + // [attr.name]="value" or attr.name="{{value}}" + this.boundUpdateInstruction( + R3.attribute, elementIndex, attrName, input, implicit, boundValue, params); } } else { - let instruction: any; - - if (inputType === BindingType.Class) { - instruction = R3.classProp; - } else { - instruction = R3.elementAttribute; - } - - this.updateInstruction(elementIndex, input.sourceSpan, instruction, () => { + // class prop + this.updateInstruction(elementIndex, input.sourceSpan, R3.classProp, () => { return [ o.literal(elementIndex), o.literal(attrName), this.convertPropertyBinding(implicit, value), ...params @@ -807,6 +808,32 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } } + /** + * Adds an update instruction for a bound property or attribute, such as `[prop]="value"` or + * `[attr.title]="value"` + */ + boundUpdateInstruction( + instruction: o.ExternalReference, elementIndex: number, attrName: string, + input: t.BoundAttribute, implicit: o.ReadVarExpr, value: any, params: any[]) { + this.updateInstruction(elementIndex, input.sourceSpan, instruction, () => { + return [o.literal(attrName), this.convertPropertyBinding(implicit, value, true), ...params]; + }); + } + + /** + * Adds an update instruction for an interpolated property or attribute, such as + * `prop="{{value}}"` or `attr.title="{{value}}"` + */ + interpolatedUpdateInstruction( + instruction: o.ExternalReference, elementIndex: number, attrName: string, + input: t.BoundAttribute, value: any, params: any[]) { + this.updateInstruction( + elementIndex, input.sourceSpan, instruction, + () => + [o.literal(attrName), + ...this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value), ...params]); + } + visitTemplate(template: t.Template) { const NG_TEMPLATE_TAG_NAME = 'ng-template'; const templateIndex = this.allocateDataSlot(); @@ -858,11 +885,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this._nestedTemplateFns.push(() => { const templateFunctionExpr = templateVisitor.buildTemplateFunction( template.children, template.variables, - this._ngContentSelectors.length + this._ngContentSelectorsOffset, template.i18n); + this._ngContentReservedSlots.length + this._ngContentSelectorsOffset, template.i18n); this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null)); - if (templateVisitor._hasNgContent) { - this._hasNgContent = true; - this._ngContentSelectors.push(...templateVisitor._ngContentSelectors); + if (templateVisitor._ngContentReservedSlots.length) { + this._ngContentReservedSlots.push(...templateVisitor._ngContentReservedSlots); } }); @@ -915,9 +941,17 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const value = text.value.visit(this._valueConverter); this.allocateBindingSlots(value); - this.updateInstruction( - nodeIndex, text.sourceSpan, R3.textBinding, - () => [o.literal(nodeIndex), this.convertPropertyBinding(o.variable(CONTEXT_NAME), value)]); + + if (value instanceof Interpolation) { + this.updateInstruction( + nodeIndex, text.sourceSpan, getTextInterpolationExpression(value), + () => this.getUpdateInstructionArguments(o.variable(CONTEXT_NAME), value)); + } else { + this.updateInstruction( + nodeIndex, text.sourceSpan, R3.textBinding, + () => + [o.literal(nodeIndex), this.convertPropertyBinding(o.variable(CONTEXT_NAME), value)]); + } } visitText(text: t.Text) { @@ -974,8 +1008,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver getVarCount() { return this._pureFunctionSlots; } getNgContentSelectors(): o.Expression|null { - return this._hasNgContent ? - this.constantPool.getConstLiteral(asLiteral(this._ngContentSelectors), true) : + return this._ngContentReservedSlots.length ? + this.constantPool.getConstLiteral(asLiteral(this._ngContentReservedSlots), true) : null; } @@ -1107,7 +1141,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver * CLASSES, class1, class2, * STYLES, style1, value1, style2, value2, * BINDINGS, name1, name2, name3, - * TEMPLATE, name4, name5, ...] + * TEMPLATE, name4, name5, name6, + * I18N, name7, name8, ...] * ``` * * Note that this function will fully ignore all synthetic (@foo) attribute values @@ -1115,7 +1150,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver */ private prepareNonRenderAttrs( inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder, - templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] { + templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [], + i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] { const alreadySeen = new Set(); const attrExprs: o.Expression[] = []; @@ -1169,6 +1205,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver templateAttrs.forEach(attr => addAttrExpr(attr.name)); } + if (i18nAttrs.length) { + attrExprs.push(o.literal(core.AttributeMarker.I18n)); + i18nAttrs.forEach(attr => addAttrExpr(attr.name)); + } + return attrExprs; } @@ -1683,6 +1724,62 @@ function getPropertyInterpolationExpression(interpolation: Interpolation) { } } +/** + * Gets the instruction to generate for an interpolated attribute + * @param interpolation An Interpolation AST + */ +function getAttributeInterpolationExpression(interpolation: Interpolation) { + switch (getInterpolationArgsLength(interpolation)) { + case 3: + return R3.attributeInterpolate1; + case 5: + return R3.attributeInterpolate2; + case 7: + return R3.attributeInterpolate3; + case 9: + return R3.attributeInterpolate4; + case 11: + return R3.attributeInterpolate5; + case 13: + return R3.attributeInterpolate6; + case 15: + return R3.attributeInterpolate7; + case 17: + return R3.attributeInterpolate8; + default: + return R3.attributeInterpolateV; + } +} + +/** + * Gets the instruction to generate for interpolated text. + * @param interpolation An Interpolation AST + */ +function getTextInterpolationExpression(interpolation: Interpolation): o.ExternalReference { + switch (getInterpolationArgsLength(interpolation)) { + case 1: + return R3.textInterpolate; + case 3: + return R3.textInterpolate1; + case 5: + return R3.textInterpolate2; + case 7: + return R3.textInterpolate3; + case 9: + return R3.textInterpolate4; + case 11: + return R3.textInterpolate5; + case 13: + return R3.textInterpolate6; + case 15: + return R3.textInterpolate7; + case 17: + return R3.textInterpolate8; + default: + return R3.textInterpolateV; + } +} + /** * Gets the number of arguments expected to be passed to a generated instruction in the case of * interpolation instructions. diff --git a/packages/compiler/test/BUILD.bazel b/packages/compiler/test/BUILD.bazel index 005800ee6e..d7ca9ec4fc 100644 --- a/packages/compiler/test/BUILD.bazel +++ b/packages/compiler/test/BUILD.bazel @@ -83,7 +83,6 @@ jasmine_node_test( deps = [ ":test_lib", ":test_node_only_lib", - "//packages/core", "//tools/testing:node", "@npm//base64-js", "@npm//source-map", diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index dad5d13b67..788ed4184e 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -32,11 +32,7 @@ export function isDirectory(data: MockFileOrDirectory | undefined): data is Mock return typeof data !== 'string'; } -const NODE_MODULES = '/node_modules/'; -const IS_GENERATED = /\.(ngfactory|ngstyle)$/; -const angularts = /@angular\/(\w|\/|-)+\.tsx?$/; const rxjs = /\/rxjs\//; -const tsxfile = /\.tsx$/; export const settings: ts.CompilerOptions = { target: ts.ScriptTarget.ES5, declaration: true, @@ -231,7 +227,7 @@ export class MockCompilerHost implements ts.CompilerHost { private traces: string[] = []; constructor(scriptNames: string[], private data: MockDirectory) { - this.scriptNames = scriptNames.slice(0); + this.scriptNames = [...scriptNames]; } // Test API @@ -748,7 +744,7 @@ function isDts(fileName: string): boolean { } function isSourceOrDts(fileName: string): boolean { - return /\.ts$/.test(fileName); + return /\.ts$/.test(fileName) && !/(ngfactory|ngstyle|ngsummary).d.ts$/.test(fileName); } function resolveNpmTreeArtifact(manifestPath: string, resolveFile = 'package.json') { @@ -778,7 +774,7 @@ export function compile( aotHost.tsFilesOnly(); } const tsSettings = {...settings, ...tsOptions}; - const program = ts.createProgram(host.scriptNames.slice(0), tsSettings, host); + const program = ts.createProgram([...host.scriptNames], tsSettings, host); preCompile(program); const {compiler, reflector} = createAotCompiler(aotHost, options, (err) => { throw err; }); const analyzedModules = @@ -792,7 +788,7 @@ export function compile( host.override(file.genFileUrl, source); } }); - const newProgram = ts.createProgram(host.scriptNames.slice(0), tsSettings, host); + const newProgram = ts.createProgram([...host.scriptNames], tsSettings, host); postCompile(newProgram); if (emit) { newProgram.emit(); diff --git a/packages/compiler/test/core_spec.ts b/packages/compiler/test/core_spec.ts index f5864584cc..c4acff371b 100644 --- a/packages/compiler/test/core_spec.ts +++ b/packages/compiler/test/core_spec.ts @@ -27,10 +27,11 @@ import * as core from '@angular/core'; typeExtends(); typeExtends(); compareRuntimeShape( - new core.ContentChild('someSelector'), compilerCore.createContentChild('someSelector')); + new core.ContentChild('someSelector', {static: false}), + compilerCore.createContentChild('someSelector', {static: false})); compareRuntimeShape( - new core.ContentChild('someSelector', {read: 'someRead'}), - compilerCore.createContentChild('someSelector', {read: 'someRead'})); + new core.ContentChild('someSelector', {read: 'someRead', static: false}), + compilerCore.createContentChild('someSelector', {read: 'someRead', static: false})); compareRuntimeShape( new core.ContentChildren('someSelector'), compilerCore.createContentChildren('someSelector')); @@ -39,10 +40,11 @@ import * as core from '@angular/core'; compilerCore.createContentChildren( 'someSelector', {read: 'someRead', descendants: false})); compareRuntimeShape( - new core.ViewChild('someSelector'), compilerCore.createViewChild('someSelector')); + new core.ViewChild('someSelector', {static: false}), + compilerCore.createViewChild('someSelector', {static: false})); compareRuntimeShape( - new core.ViewChild('someSelector', {read: 'someRead'}), - compilerCore.createViewChild('someSelector', {read: 'someRead'})); + new core.ViewChild('someSelector', {read: 'someRead', static: false}), + compilerCore.createViewChild('someSelector', {read: 'someRead', static: false})); compareRuntimeShape( new core.ViewChildren('someSelector'), compilerCore.createViewChildren('someSelector')); compareRuntimeShape( diff --git a/packages/compiler/test/directive_resolver_spec.ts b/packages/compiler/test/directive_resolver_spec.ts index 4d211643f9..e0ad64bb9f 100644 --- a/packages/compiler/test/directive_resolver_spec.ts +++ b/packages/compiler/test/directive_resolver_spec.ts @@ -68,15 +68,15 @@ class SomeDirectiveWithViewChildren { c: any; } -@Directive({selector: 'someDirective', queries: {'c': new ContentChild('c')}}) +@Directive({selector: 'someDirective', queries: {'c': new ContentChild('c', {static: false})}}) class SomeDirectiveWithContentChild { - @ContentChild('a') a: any; + @ContentChild('a', {static: false}) a: any; c: any; } -@Directive({selector: 'someDirective', queries: {'c': new ViewChild('c')}}) +@Directive({selector: 'someDirective', queries: {'c': new ViewChild('c', {static: false})}}) class SomeDirectiveWithViewChild { - @ViewChild('a') a: any; + @ViewChild('a', {static: false}) a: any; c: any; } @@ -408,37 +408,41 @@ class SomeDirectiveWithoutMetadata {} it('should append ContentChild', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild); - expect(directiveMetadata.queries) - .toEqual({'c': new ContentChild('c'), 'a': new ContentChild('a')}); + expect(directiveMetadata.queries).toEqual({ + 'c': new ContentChild('c', {static: false}), + 'a': new ContentChild('a', {static: false}) + }); }); it('should append ViewChild', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild); - expect(directiveMetadata.queries) - .toEqual({'c': new ViewChild('c'), 'a': new ViewChild('a')}); + expect(directiveMetadata.queries).toEqual({ + 'c': new ViewChild('c', {static: false}), + 'a': new ViewChild('a', {static: false}) + }); }); it('should support inheriting queries', () => { @Directive({selector: 'p'}) class Parent { - @ContentChild('p1') + @ContentChild('p1', {static: false}) p1: any; - @ContentChild('p21') + @ContentChild('p21', {static: false}) p2: any; } class Child extends Parent { - @ContentChild('p22') + @ContentChild('p22', {static: false}) p2: any; - @ContentChild('p3') + @ContentChild('p3', {static: false}) p3: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.queries).toEqual({ - 'p1': new ContentChild('p1'), - 'p2': new ContentChild('p22'), - 'p3': new ContentChild('p3') + 'p1': new ContentChild('p1', {static: false}), + 'p2': new ContentChild('p22', {static: false}), + 'p3': new ContentChild('p3', {static: false}) }); }); }); diff --git a/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel b/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel index 773d18ed6d..ffe09d7d9c 100644 --- a/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/injectable-pipe/google3/BUILD.bazel @@ -8,5 +8,6 @@ ts_library( deps = [ "//packages/core/schematics/migrations/injectable-pipe", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/injectable-pipe/index.ts b/packages/core/schematics/migrations/injectable-pipe/index.ts index 5c4615bd90..51c088677b 100644 --- a/packages/core/schematics/migrations/injectable-pipe/index.ts +++ b/packages/core/schematics/migrations/injectable-pipe/index.ts @@ -47,7 +47,10 @@ function runInjectablePipeMigration(tree: Tree, tsconfigPath: string, basePath: // source files, it can end up updating query definitions multiple times. host.readFile = fileName => { const buffer = tree.read(relative(basePath, fileName)); - return buffer ? buffer.toString() : undefined; + // Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which + // which breaks the CLI UpdateRecorder. + // See: https://github.com/angular/angular/pull/30719 + return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined; }; const program = ts.createProgram(parsed.fileNames, parsed.options, host); diff --git a/packages/core/schematics/migrations/move-document/index.ts b/packages/core/schematics/migrations/move-document/index.ts index ffb2a59079..30d0cc5597 100644 --- a/packages/core/schematics/migrations/move-document/index.ts +++ b/packages/core/schematics/migrations/move-document/index.ts @@ -48,7 +48,10 @@ function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: st // source files, it can end up updating query definitions multiple times. host.readFile = fileName => { const buffer = tree.read(relative(basePath, fileName)); - return buffer ? buffer.toString() : undefined; + // Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which + // which breaks the CLI UpdateRecorder. + // See: https://github.com/angular/angular/pull/30719 + return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined; }; const program = ts.createProgram(parsed.fileNames, parsed.options, host); diff --git a/packages/core/schematics/migrations/static-queries/BUILD.bazel b/packages/core/schematics/migrations/static-queries/BUILD.bazel index f8fa0a26ba..25993f3bf4 100644 --- a/packages/core/schematics/migrations/static-queries/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/node", "@npm//rxjs", diff --git a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel index a588549552..9467244813 100644 --- a/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/static-queries/google3/BUILD.bazel @@ -9,5 +9,6 @@ ts_library( "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/utils", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/static-queries/index.ts b/packages/core/schematics/migrations/static-queries/index.ts index 089d369d7c..207531bbeb 100644 --- a/packages/core/schematics/migrations/static-queries/index.ts +++ b/packages/core/schematics/migrations/static-queries/index.ts @@ -21,9 +21,14 @@ import {QueryTemplateStrategy} from './strategies/template_strategy/template_str import {QueryTestStrategy} from './strategies/test_strategy/test_strategy'; import {TimingStrategy} from './strategies/timing-strategy'; import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy'; -import {SELECTED_STRATEGY, promptForMigrationStrategy} from './strategy_prompt'; import {getTransformedQueryCallExpr} from './transform'; +enum SELECTED_STRATEGY { + TEMPLATE, + USAGE, + TESTS, +} + interface AnalyzedProject { program: ts.Program; host: ts.CompilerHost; @@ -49,32 +54,33 @@ async function runMigration(tree: Tree, context: SchematicContext) { const basePath = process.cwd(); const logger = context.logger; - logger.info('------ Static Query migration ------'); - logger.info('In preparation for Ivy, developers can now explicitly specify the'); - logger.info('timing of their queries. Read more about this here:'); - logger.info('https://github.com/angular/angular/pull/28810'); - logger.info(''); + logger.info('------ Static Query Migration ------'); + logger.info('With Angular version 8, developers need to'); + logger.info('explicitly specify the timing of ViewChild and'); + logger.info('ContentChild queries. Read more about this here:'); + logger.info('https://v8.angular.io/guide/static-query-migration'); if (!buildPaths.length && !testPaths.length) { throw new SchematicsException( 'Could not find any tsconfig file. Cannot migrate queries ' + - 'to explicit timing.'); + 'to add static flag.'); } + const analyzedFiles = new Set(); const buildProjects = new Set(); const failures = []; + const strategy = process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] === 'true' ? + SELECTED_STRATEGY.USAGE : + SELECTED_STRATEGY.TEMPLATE; for (const tsconfigPath of buildPaths) { - const project = analyzeProject(tree, tsconfigPath, basePath); + const project = analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger); if (project) { buildProjects.add(project); } } - // In case there are projects which contain queries that need to be migrated, - // we want to prompt for the migration strategy and run the migration. if (buildProjects.size) { - const strategy = await promptForMigrationStrategy(logger); for (let project of Array.from(buildProjects.values())) { failures.push(...await runStaticQueryMigration(tree, project, strategy, logger)); } @@ -83,7 +89,7 @@ async function runMigration(tree: Tree, context: SchematicContext) { // For the "test" tsconfig projects we always want to use the test strategy as // we can't detect the proper timing within spec files. for (const tsconfigPath of testPaths) { - const project = await analyzeProject(tree, tsconfigPath, basePath); + const project = await analyzeProject(tree, tsconfigPath, basePath, analyzedFiles, logger); if (project) { failures.push( ...await runStaticQueryMigration(tree, project, SELECTED_STRATEGY.TESTS, logger)); @@ -91,8 +97,11 @@ async function runMigration(tree: Tree, context: SchematicContext) { } if (failures.length) { + logger.info(''); logger.info('Some queries could not be migrated automatically. Please go'); - logger.info('through those manually and apply the appropriate timing:'); + logger.info('through these manually and apply the appropriate timing.'); + logger.info('For more info on how to choose a flag, please see: '); + logger.info('https://v8.angular.io/guide/static-query-migration'); failures.forEach(failure => logger.warn(`⮑ ${failure}`)); } @@ -103,7 +112,9 @@ async function runMigration(tree: Tree, context: SchematicContext) { * Analyzes the given TypeScript project by looking for queries that need to be * migrated. In case there are no queries that can be migrated, null is returned. */ -function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): +function analyzeProject( + tree: Tree, tsconfigPath: string, basePath: string, analyzedFiles: Set, + logger: logging.LoggerApi): AnalyzedProject|null { const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath)); const host = ts.createCompilerHost(parsed.options, true); @@ -114,10 +125,27 @@ function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): // source files, it can end up updating query definitions multiple times. host.readFile = fileName => { const buffer = tree.read(relative(basePath, fileName)); - return buffer ? buffer.toString() : undefined; + // Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which + // which breaks the CLI UpdateRecorder. + // See: https://github.com/angular/angular/pull/30719 + return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined; }; const program = ts.createProgram(parsed.fileNames, parsed.options, host); + const syntacticDiagnostics = program.getSyntacticDiagnostics(); + + // Syntactic TypeScript errors can throw off the query analysis and therefore we want + // to notify the developer that we couldn't analyze parts of the project. Developers + // can just re-run the migration after fixing these failures. + if (syntacticDiagnostics.length) { + logger.warn( + `\nTypeScript project "${tsconfigPath}" has syntactical errors which could cause ` + + `an incomplete migration. Please fix the following failures and rerun the migration:`); + logger.error(ts.formatDiagnostics(syntacticDiagnostics, host)); + logger.info( + 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n'); + } + const typeChecker = program.getTypeChecker(); const sourceFiles = program.getSourceFiles().filter( f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); @@ -125,7 +153,16 @@ function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): // Analyze all project source-files and collect all queries that // need to be migrated. - sourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile)); + sourceFiles.forEach(sourceFile => { + const relativePath = relative(basePath, sourceFile.fileName); + + // Only look for queries within the current source files if the + // file has not been analyzed before. + if (!analyzedFiles.has(relativePath)) { + analyzedFiles.add(relativePath); + queryVisitor.visitNode(sourceFile); + } + }); if (queryVisitor.resolvedQueries.size === 0) { return null; @@ -142,7 +179,7 @@ function analyzeProject(tree: Tree, tsconfigPath: string, basePath: string): */ async function runStaticQueryMigration( tree: Tree, project: AnalyzedProject, selectedStrategy: SELECTED_STRATEGY, - logger: logging.LoggerApi) { + logger: logging.LoggerApi): Promise { const {sourceFiles, typeChecker, host, queryVisitor, tsconfigPath, basePath} = project; const printer = ts.createPrinter(); const failureMessages: string[] = []; @@ -179,22 +216,22 @@ async function runStaticQueryMigration( try { strategy.setup(); } catch (e) { + if (selectedStrategy === SELECTED_STRATEGY.TEMPLATE) { + logger.warn( + `\nThe template migration strategy uses the Angular compiler ` + + `internally and therefore projects that no longer build successfully after ` + + `the update cannot use the template migration strategy. Please ensure ` + + `there are no AOT compilation errors.\n`); + } // In case the strategy could not be set up properly, we just exit the // migration. We don't want to throw an exception as this could mean // that other migrations are interrupted. logger.warn( `Could not setup migration strategy for "${project.tsconfigPath}". The ` + - `following error has been reported:`); - if (selectedStrategy === SELECTED_STRATEGY.TEMPLATE) { - logger.warn( - `The template migration strategy uses the Angular compiler ` + - `internally and therefore projects that no longer build successfully after ` + - `the update cannot use the template migration strategy. Please ensure ` + - `there are no AOT compilation errors.`); - } - logger.error(e); + `following error has been reported:\n`); + logger.error(`${e.toString()}\n`); logger.info( - 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"'); + 'Migration can be rerun with: "ng update @angular/core --from 7 --to 8 --migrate-only"\n'); return []; } diff --git a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts b/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts index f7c0084470..4028d01866 100644 --- a/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts +++ b/packages/core/schematics/migrations/static-queries/strategies/template_strategy/template_strategy.ts @@ -57,13 +57,9 @@ export class QueryTemplateStrategy implements TimingStrategy { // used to determine the timing for registered queries. const analyzedModules = (aotProgram as any)['analyzedModules'] as NgAnalyzedModules; - const ngDiagnostics = [ - ...aotProgram.getNgStructuralDiagnostics(), - ...aotProgram.getNgSemanticDiagnostics(), - ]; - - if (ngDiagnostics.length) { - throw this._createDiagnosticsError(ngDiagnostics); + const ngStructuralDiagnostics = aotProgram.getNgStructuralDiagnostics(); + if (ngStructuralDiagnostics.length) { + throw this._createDiagnosticsError(ngStructuralDiagnostics); } analyzedModules.files.forEach(file => { @@ -178,12 +174,8 @@ export class QueryTemplateStrategy implements TimingStrategy { .template; } - private _createDiagnosticsError(diagnostics: (ts.Diagnostic|Diagnostic)[]) { - return new Error( - `Could not create Angular AOT compiler to determine query timing.\n` + - `The following diagnostics were detected:\n` + - `${diagnostics.map(d => d.messageText).join(`\n `)}\n` + - `Please make sure that there is no AOT compilation failure.`); + private _createDiagnosticsError(diagnostics: ReadonlyArray) { + return new Error(ts.formatDiagnostics(diagnostics as ts.Diagnostic[], this.host)); } private _getViewQueryUniqueKey(filePath: string, className: string, propName: string) { diff --git a/packages/core/schematics/migrations/static-queries/strategy_prompt.ts b/packages/core/schematics/migrations/static-queries/strategy_prompt.ts deleted file mode 100644 index e0cbf514a9..0000000000 --- a/packages/core/schematics/migrations/static-queries/strategy_prompt.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {logging} from '@angular-devkit/core'; - -import {getInquirer, supportsPrompt} from '../../utils/schematics_prompt'; - -export enum SELECTED_STRATEGY { - TEMPLATE, - USAGE, - TESTS, -} - -/** - * Prompts the user for the migration strategy that should be used. Defaults to the - * template strategy as it provides a migration with rare manual corrections. - * */ -export async function promptForMigrationStrategy(logger: logging.LoggerApi) { - if (supportsPrompt()) { - logger.info('There are two available migration strategies that can be selected:'); - logger.info(' • Template strategy - migration tool (short-term gains, rare corrections)'); - logger.info(' • Usage strategy - best practices (long-term gains, manual corrections)'); - logger.info('For an easy migration, the template strategy is recommended. The usage'); - logger.info('strategy can be used for best practices and a code base that will be more'); - logger.info('flexible to changes going forward.'); - const {strategyName} = await getInquirer().prompt<{strategyName: string}>({ - type: 'list', - name: 'strategyName', - message: 'What migration strategy do you want to use?', - choices: [ - {name: 'Template strategy', value: 'template'}, {name: 'Usage strategy', value: 'usage'} - ], - default: 'template', - }); - logger.info(''); - return strategyName === 'usage' ? SELECTED_STRATEGY.USAGE : SELECTED_STRATEGY.TEMPLATE; - } else { - // In case prompts are not supported, we still want to allow developers to opt - // into the usage strategy by specifying an environment variable. The tests also - // use the environment variable as there is no headless way to select via prompt. - return !!process.env['NG_STATIC_QUERY_USAGE_STRATEGY'] ? SELECTED_STRATEGY.USAGE : - SELECTED_STRATEGY.TEMPLATE; - } -} diff --git a/packages/core/schematics/migrations/static-queries/transform.ts b/packages/core/schematics/migrations/static-queries/transform.ts index 139df0afa2..f60875799a 100644 --- a/packages/core/schematics/migrations/static-queries/transform.ts +++ b/packages/core/schematics/migrations/static-queries/transform.ts @@ -72,7 +72,7 @@ export function getTransformedQueryCallExpr( // we create a transformation failure message that shows developers that they need // to set the query timing manually to the determined query timing. if (timing !== null) { - failureMessage = 'Cannot update query declaration to explicit timing. Please manually ' + + failureMessage = 'Cannot update query to set explicit timing. Please manually ' + `set the query timing to: "{static: ${(timing === QueryTiming.STATIC).toString()}}"`; } } diff --git a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel b/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel index a45b58f070..5e0003affc 100644 --- a/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel +++ b/packages/core/schematics/migrations/template-var-assignment/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( deps = [ "//packages/compiler", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/node", "@npm//typescript", diff --git a/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel b/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel index 83b75a91e6..2beb6d06a4 100644 --- a/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/template-var-assignment/google3/BUILD.bazel @@ -10,5 +10,6 @@ ts_library( "//packages/core/schematics/utils", "//packages/core/schematics/utils/tslint", "@npm//tslint", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/migrations/template-var-assignment/index.ts b/packages/core/schematics/migrations/template-var-assignment/index.ts index 16cbf4b056..a7fce2081b 100644 --- a/packages/core/schematics/migrations/template-var-assignment/index.ts +++ b/packages/core/schematics/migrations/template-var-assignment/index.ts @@ -19,8 +19,7 @@ import {analyzeResolvedTemplate} from './analyze_template'; type Logger = logging.LoggerApi; -const README_URL = - 'https://github.com/angular/angular/tree/master/packages/core/schematics/migrations/template-var-assignment/README.md'; +const README_URL = 'https://v8.angular.io/guide/deprecations#cannot-assign-to-template-variables'; const FAILURE_MESSAGE = `Found assignment to template variable.`; /** Entry point for the V8 template variable assignment schematic. */ @@ -54,7 +53,10 @@ function runTemplateVariableAssignmentCheck( // program to be based on the file contents in the virtual file tree. host.readFile = fileName => { const buffer = tree.read(relative(basePath, fileName)); - return buffer ? buffer.toString() : undefined; + // Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which + // which breaks the CLI UpdateRecorder. + // See: https://github.com/angular/angular/pull/30719 + return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined; }; const program = ts.createProgram(parsed.fileNames, parsed.options, host); diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index f61d48e0dc..b064f3a3af 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -17,6 +17,7 @@ ts_library( "//packages/core/schematics/migrations/template-var-assignment", "//packages/core/schematics/migrations/template-var-assignment/google3", "//packages/core/schematics/utils", + "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/shelljs", "@npm//tslint", diff --git a/packages/core/schematics/test/injectable_pipe_migration_spec.ts b/packages/core/schematics/test/injectable_pipe_migration_spec.ts index a37399414e..4cdf8c7918 100644 --- a/packages/core/schematics/test/injectable_pipe_migration_spec.ts +++ b/packages/core/schematics/test/injectable_pipe_migration_spec.ts @@ -46,7 +46,7 @@ describe('injectable pipe migration', () => { shx.rm('-r', tmpDirPath); }); - it('should add @Injectable to pipes that do not have it', () => { + it('should add @Injectable to pipes that do not have it', async() => { writeFile('/index.ts', ` import { Pipe } from '@angular/core'; @@ -55,12 +55,26 @@ describe('injectable pipe migration', () => { } `); + await runMigration(); + expect(tree.readContent('/index.ts')) + .toMatch(/@Injectable\(\)\s+@Pipe\(\{ name: 'myPipe' \}\)\s+export class MyPipe/); + }); + + it('should add @Injectable to pipes that do not have it (BOM)', () => { + writeFile('/index.ts', `\uFEFF + import { Pipe } from '@angular/core'; + + @Pipe({ name: 'myPipe' }) + export class MyPipe { + } + `); + runMigration(); expect(tree.readContent('/index.ts')) .toMatch(/@Injectable\(\)\s+@Pipe\(\{ name: 'myPipe' \}\)\s+export class MyPipe/); }); - it('should add an import for Injectable to the @angular/core import declaration', () => { + it('should add an import for Injectable to the @angular/core import declaration', async() => { writeFile('/index.ts', ` import { Pipe } from '@angular/core'; @@ -69,14 +83,14 @@ describe('injectable pipe migration', () => { } `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); expect(content).toContain('import { Pipe, Injectable } from \'@angular/core\''); expect((content.match(/import/g) || []).length).toBe(1, 'Expected only one import statement'); }); - it('should not add an import for Injectable if it is imported already', () => { + it('should not add an import for Injectable if it is imported already', async() => { writeFile('/index.ts', ` import { Pipe, Injectable, NgModule } from '@angular/core'; @@ -85,12 +99,12 @@ describe('injectable pipe migration', () => { } `); - runMigration(); + await runMigration(); expect(tree.readContent('/index.ts')) .toContain('import { Pipe, Injectable, NgModule } from \'@angular/core\''); }); - it('should do nothing if the pipe is marked as injectable already', () => { + it('should do nothing if the pipe is marked as injectable already', async() => { const source = ` import { Injectable, Pipe } from '@angular/core'; @@ -101,11 +115,11 @@ describe('injectable pipe migration', () => { `; writeFile('/index.ts', source); - runMigration(); + await runMigration(); expect(tree.readContent('/index.ts')).toBe(source); }); - it('should not add @Injectable if @Pipe was not imported from @angular/core', () => { + it('should not add @Injectable if @Pipe was not imported from @angular/core', async() => { const source = ` import { Pipe } from '@not-angular/core'; @@ -115,7 +129,7 @@ describe('injectable pipe migration', () => { `; writeFile('/index.ts', source); - runMigration(); + await runMigration(); expect(tree.readContent('/index.ts')).toBe(source); }); @@ -123,5 +137,7 @@ describe('injectable pipe migration', () => { host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); } - function runMigration() { runner.runSchematic('migration-injectable-pipe', {}, tree); } + function runMigration() { + runner.runSchematicAsync('migration-injectable-pipe', {}, tree).toPromise(); + } }); diff --git a/packages/core/schematics/test/move_document_migration_spec.ts b/packages/core/schematics/test/move_document_migration_spec.ts index 85f55411ac..7254158615 100644 --- a/packages/core/schematics/test/move_document_migration_spec.ts +++ b/packages/core/schematics/test/move_document_migration_spec.ts @@ -47,11 +47,24 @@ describe('move-document migration', () => { }); describe('move-document', () => { - it('should properly apply import replacement', () => { + it('should properly apply import replacement', async() => { writeFile('/index.ts', ` import {DOCUMENT} from '@angular/platform-browser'; `); + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain(`import { DOCUMENT } from "@angular/common";`); + expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); + }); + + it('should properly apply import replacement (BOM)', () => { + writeFile('/index.ts', `\uFEFF + import {DOCUMENT} from '@angular/platform-browser'; + `); + runMigration(); const content = tree.readContent('/index.ts'); @@ -60,7 +73,7 @@ describe('move-document migration', () => { expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); }); - it('should properly apply import replacement with existing import', () => { + it('should properly apply import replacement with existing import', async() => { writeFile('/index.ts', ` import {DOCUMENT} from '@angular/platform-browser'; import {someImport} from '@angular/common'; @@ -71,7 +84,7 @@ describe('move-document migration', () => { import {DOCUMENT} from '@angular/platform-browser'; `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); const contentReverse = tree.readContent('/reverse.ts'); @@ -83,7 +96,7 @@ describe('move-document migration', () => { expect(contentReverse).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`); }); - it('should properly apply import replacement with existing import w/ comments', () => { + it('should properly apply import replacement with existing import w/ comments', async() => { writeFile('/index.ts', ` /** * this is a comment @@ -92,7 +105,7 @@ describe('move-document migration', () => { import {DOCUMENT} from '@angular/platform-browser'; `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); @@ -102,14 +115,14 @@ describe('move-document migration', () => { expect(content).toMatch(/.*this is a comment.*/); }); - it('should properly apply import replacement with existing and redundant imports', () => { + it('should properly apply import replacement with existing and redundant imports', async() => { writeFile('/index.ts', ` import {DOCUMENT} from '@angular/platform-browser'; import {anotherImport} from '@angular/platform-browser-dynamic'; import {someImport} from '@angular/common'; `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); @@ -118,13 +131,13 @@ describe('move-document migration', () => { }); it('should properly apply import replacement with existing import and leave original import', - () => { + async() => { writeFile('/index.ts', ` import {DOCUMENT, anotherImport} from '@angular/platform-browser'; import {someImport} from '@angular/common'; `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); @@ -132,13 +145,13 @@ describe('move-document migration', () => { expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`); }); - it('should properly apply import replacement with existing import and alias', () => { + it('should properly apply import replacement with existing import and alias', async() => { writeFile('/index.ts', ` import {DOCUMENT as doc, anotherImport} from '@angular/platform-browser'; import {someImport} from '@angular/common'; `); - runMigration(); + await runMigration(); const content = tree.readContent('/index.ts'); @@ -151,5 +164,7 @@ describe('move-document migration', () => { host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); } - function runMigration() { runner.runSchematic('migration-v8-move-document', {}, tree); } + function runMigration() { + runner.runSchematicAsync('migration-v8-move-document', {}, tree).toPromise(); + } }); diff --git a/packages/core/schematics/test/project_tsconfig_paths_spec.ts b/packages/core/schematics/test/project_tsconfig_paths_spec.ts index 0091ce0070..0ebf15276d 100644 --- a/packages/core/schematics/test/project_tsconfig_paths_spec.ts +++ b/packages/core/schematics/test/project_tsconfig_paths_spec.ts @@ -25,6 +25,26 @@ describe('project tsconfig paths', () => { expect(getProjectTsConfigPaths(testTree).buildPaths).toEqual(['my-custom-config.json']); }); + it('should be able to read workspace configuration which is using JSON5 features', () => { + testTree.create('/my-build-config.json', ''); + testTree.create('/angular.json', `{ + // Comments, unquoted properties or trailing commas are only supported in JSON5. + projects: { + with_tests: { + targets: { + build: { + options: { + tsConfig: './my-build-config.json', + } + } + } + } + }, + }`); + + expect(getProjectTsConfigPaths(testTree).buildPaths).toEqual(['my-build-config.json']); + }); + it('should detect test tsconfig path inside of angular.json file', () => { testTree.create('/my-test-config.json', ''); testTree.create('/angular.json', JSON.stringify({ diff --git a/packages/core/schematics/test/static_queries_migration_template_spec.ts b/packages/core/schematics/test/static_queries_migration_template_spec.ts index e8f6b96815..89956ce4dc 100644 --- a/packages/core/schematics/test/static_queries_migration_template_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_template_spec.ts @@ -158,6 +158,29 @@ describe('static-queries migration with template strategy', () => { .toContain(`@ViewChild('myTmpl', { static: true }) query: any;`); }); + it('should detect queries selecting ng-template as static (BOM)', async() => { + writeFile('/index.ts', `\uFEFF + import {Component, NgModule, ViewChild} from '@angular/core'; + + @Component({template: \` + + My template + + \`}) + export class MyComp { + private @ViewChild('myTmpl') query: any; + } + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('myTmpl', { static: true }) query: any;`); + }); + it('should detect queries selecting component view providers through string token', async() => { writeFile('/index.ts', ` import {Component, Directive, NgModule, ViewChild} from '@angular/core'; @@ -498,6 +521,115 @@ describe('static-queries migration with template strategy', () => { /^⮑ {3}index.ts@5:11: Multiple components use the query with different timings./); }); + it('should be able to migrate an application with type checking failure which ' + + 'does not affect analysis', + async() => { + // Fakes the `@angular/package` by creating a `ViewChild` decorator + // function that requires developers to specify the "static" flag. + writeFile('/node_modules/@angular/core/index.d.ts', ` + export interface ViewChildDecorator { + (selector: Type | Function | string, opts: { + static: boolean; + read?: any; + }): any; + } + + export declare const ViewChild: ViewChildDecorator; + `); + + writeFile('/index.ts', ` + import {NgModule, Component, ViewChild} from '@angular/core'; + + @Component({ + template: '

' + }) + export class MyComp { + @ViewChild('myRef') query: any; + } + `); + + writeFile('/my-module.ts', ` + import {NgModule} from '@angular/core'; + import {MyComp} from './index'; + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + await runMigration(); + + expect(errorOutput.length).toBe(0); + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('myRef', { static: false }) query: any;`); + }); + + it('should be able to migrate applications with template type checking failure ' + + 'which does not affect analysis', + async() => { + writeFile('/index.ts', ` + import {NgModule, Component, ViewChild} from '@angular/core'; + + @Component({ + template: '

{{myVar.hello()}}

' + }) + export class MyComp { + // This causes a type checking exception as the template + // tries to call a function called "hello()" on this variable. + myVar: boolean = false; + + @ViewChild('myRef') query: any; + } + `); + + writeFile('/my-module.ts', ` + import {NgModule} from '@angular/core'; + import {MyComp} from './index'; + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + await runMigration(); + + expect(errorOutput.length).toBe(0); + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('myRef', { static: true }) query: any;`); + }); + + it('should notify user if project has syntax errors which can affect analysis', async() => { + writeFile('/index.ts', ` + import {Component, ViewChild} from '@angular/core'; + + @Component({ + template: '

' + }) + export class MyComp { + @ViewChild('myRef') query: any; + } + `); + + writeFile('/file-with-syntax-error.ts', ` + export classX ClassWithSyntaxError { + // ... + } + `); + + writeFile('/my-module.ts', ` + import {NgModule} from '@angular/core'; + import {MyComp} from './index'; + + @NgModule({declarations: [MyComp]}) + export class MyModule {} + `); + + await runMigration(); + + expect(errorOutput.length).toBe(1); + expect(errorOutput[0]).toMatch(/file-with-syntax-error\.ts\(2,9\): error TS1128.*/); + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('myRef', { static: true }) query: any;`); + }); + it('should gracefully exit migration if queries could not be analyzed', async() => { writeFile('/index.ts', ` import {Component, ViewChild} from '@angular/core'; @@ -515,8 +647,6 @@ describe('static-queries migration with template strategy', () => { await runMigration(); expect(errorOutput.length).toBe(1); - expect(errorOutput[0]) - .toMatch(/^Error: Could not create Angular AOT compiler to determine query timing./); expect(errorOutput[0]).toMatch(/Cannot determine the module for class MyComp/); }); @@ -604,7 +734,7 @@ describe('static-queries migration with template strategy', () => { .toContain(`@ViewChild('myRef', /* TODO: add static flag */ myOptionsVar) query: any;`); expect(warnOutput.length).toBe(1); expect(warnOutput[0]) - .toMatch(/^⮑ {3}index.ts@8:11: Cannot update query declaration to explicit timing./); + .toMatch(/^⮑ {3}index.ts@8:11: Cannot update query to set explicit timing./); expect(warnOutput[0]).toMatch(/Please manually set the query timing to.*static: true/); }); @@ -673,15 +803,49 @@ describe('static-queries migration with template strategy', () => { export class MyModule {} `); - spyOn(console, 'error').and.callThrough(); - await runMigration(); - expect(console.error).toHaveBeenCalledTimes(0); + expect(errorOutput.length).toBe(0); expect(tree.readContent('/src/test.ts')) .toContain(`@ViewChild('test', /* TODO: add static flag */ {}) query: any;`); expect(tree.readContent('/src/app.component.ts')) .toContain(`@ViewChild('test', { static: true }) query: any;`); }); + + it('should not fall back to test strategy if selected strategy fails', async() => { + writeFile('/src/tsconfig.spec.json', JSON.stringify({ + compilerOptions: { + experimentalDecorators: true, + lib: ['es2015'], + }, + files: [ + 'test.ts', + ], + })); + + writeFile('/src/test.ts', `import * as mod from './app.module';`); + writeFile('/src/app.component.ts', ` + import {Component, ViewChild} from '@angular/core'; + + @Component({template: 'Test'}) + export class AppComponent { + @ViewChild('test') query: any; + } + `); + + writeFile('/src/app.module.ts', ` + import {NgModule} from '@angular/core'; + import {AppComponent} from './app.component'; + + @NgModule({declarations: [AppComponent, ThisCausesAnError]}) + export class MyModule {} + `); + + await runMigration(); + + expect(errorOutput.length).toBe(1); + expect(errorOutput[0]).toMatch(/Unexpected value 'undefined'/); + expect(tree.readContent('/src/app.component.ts')).toContain(`@ViewChild('test') query: any;`); + }); }); }); diff --git a/packages/core/schematics/test/static_queries_migration_usage_spec.ts b/packages/core/schematics/test/static_queries_migration_usage_spec.ts index fa92fa1e0b..02946c884d 100644 --- a/packages/core/schematics/test/static_queries_migration_usage_spec.ts +++ b/packages/core/schematics/test/static_queries_migration_usage_spec.ts @@ -127,6 +127,26 @@ describe('static-queries migration with usage strategy', () => { .toContain(`@ContentChild('test', { static: false }) query: any;`); }); + it('should not mark content queries used in "ngAfterContentInit" as static (BOM)', async() => { + writeFile('/index.ts', `\uFEFF + import {Component, ContentChild} from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + @ContentChild('test') query: any; + + ngAfterContentInit() { + this.query.classList.add('test'); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain(`@ContentChild('test', { static: false }) query: any;`); + }); + it('should not mark content queries used in "ngAfterContentChecked" as static', async() => { writeFile('/index.ts', ` import {Component, ContentChild} from '@angular/core'; @@ -1506,23 +1526,6 @@ describe('static-queries migration with usage strategy', () => { .toContain(`@${queryType}('test', { static: false }) query: any;`); }); - it(`should not prompt for migration strategy if no @${queryType} query is used`, async() => { - writeFile('/index.ts', ` - import {Component, ${queryType}} from '@angular/core'; - - @Component({template: ''}) - export class NoQueriesDeclared { - } - `); - - const testModule = require('../migrations/static-queries/strategy_prompt'); - spyOn(testModule, 'promptForMigrationStrategy').and.callThrough(); - - await runMigration(); - - expect(testModule.promptForMigrationStrategy).toHaveBeenCalledTimes(0); - }); - it('should support function call with default parameter value', async() => { writeFile('/index.ts', ` import {Component, ${queryType}} from '@angular/core'; diff --git a/packages/core/schematics/test/template_var_assignment_migration_spec.ts b/packages/core/schematics/test/template_var_assignment_migration_spec.ts index 742ced7e15..f0af511a57 100644 --- a/packages/core/schematics/test/template_var_assignment_migration_spec.ts +++ b/packages/core/schematics/test/template_var_assignment_migration_spec.ts @@ -59,10 +59,10 @@ describe('template variable assignment migration', () => { } function runMigration() { - runner.runSchematic('migration-v8-template-local-variables', {}, tree); + return runner.runSchematicAsync('migration-v8-template-local-variables', {}, tree).toPromise(); } - it('should warn for two-way data binding variable assignment', () => { + it('should warn for two-way data binding variable assignment', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -72,13 +72,13 @@ describe('template variable assignment migration', () => { export class MyComp {} `); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(1); expect(warnOutput[0]).toMatch(/^⮑ {3}index.ts@5:69: Found assignment/); }); - it('should warn for two-way data binding assigning to "as" variable', () => { + it('should warn for two-way data binding assigning to "as" variable', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -94,13 +94,13 @@ describe('template variable assignment migration', () => {
`); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(1); expect(warnOutput).toMatch(/^⮑ {3}tmpl.html@3:31: Found assignment/); }); - it('should warn for bound event assignments to "as" variable', () => { + it('should warn for bound event assignments to "as" variable', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -117,14 +117,14 @@ describe('template variable assignment migration', () => {
`); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(2); expect(warnOutput[0]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@3:25: Found assignment/); expect(warnOutput[1]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@4:25: Found assignment/); }); - it('should warn for bound event assignments to template "let" variables', () => { + it('should warn for bound event assignments to template "let" variables', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -141,14 +141,14 @@ describe('template variable assignment migration', () => { `); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(2); expect(warnOutput[0]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@3:25: Found assignment/); expect(warnOutput[1]).toMatch(/^⮑ {3}sub_dir\/tmpl.html@4:25: Found assignment/); }); - it('should not warn for bound event assignments to component property', () => { + it('should not warn for bound event assignments to component property', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -160,13 +160,14 @@ describe('template variable assignment migration', () => { writeFile('/sub_dir/tmpl.html', ``); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(0); }); - it('should not warn for bound event assignments to template variable object property', () => { - writeFile('/index.ts', ` + it('should not warn for bound event assignments to template variable object property', + async() => { + writeFile('/index.ts', ` import {Component} from '@angular/core'; @Component({ @@ -175,17 +176,17 @@ describe('template variable assignment migration', () => { export class MyComp {} `); - writeFile('/sub_dir/tmpl.html', ` + writeFile('/sub_dir/tmpl.html', ` `); - runMigration(); + await runMigration(); - expect(warnOutput.length).toBe(0); - }); + expect(warnOutput.length).toBe(0); + }); it('should not warn for property writes with template variable name but different receiver', - () => { + async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -205,13 +206,14 @@ describe('template variable assignment migration', () => { `); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(0); }); - it('should not warn for property writes with template variable name but different scope', () => { - writeFile('/index.ts', ` + it('should not warn for property writes with template variable name but different scope', + async() => { + writeFile('/index.ts', ` import {Component} from '@angular/core'; @Component({ @@ -222,18 +224,18 @@ describe('template variable assignment migration', () => { } `); - writeFile('/sub_dir/tmpl.html', ` + writeFile('/sub_dir/tmpl.html', ` `); - runMigration(); + await runMigration(); - expect(warnOutput.length).toBe(0); - }); + expect(warnOutput.length).toBe(0); + }); - it('should not throw an error if a detected template fails parsing', () => { + it('should not throw an error if a detected template fails parsing', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -245,12 +247,12 @@ describe('template variable assignment migration', () => { writeFile('/sub_dir/tmpl.html', ``); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(0); }); - it('should be able to report multiple templates within the same source file', () => { + it('should be able to report multiple templates within the same source file', async() => { writeFile('/index.ts', ` import {Component} from '@angular/core'; @@ -265,7 +267,7 @@ describe('template variable assignment migration', () => { export class MyComp2 {} `); - runMigration(); + await runMigration(); expect(warnOutput.length).toBe(2); expect(warnOutput[0]).toMatch(/^⮑ {3}index.ts@5:56: Found assignment/); diff --git a/packages/core/schematics/utils/BUILD.bazel b/packages/core/schematics/utils/BUILD.bazel index 44b3e073e8..5ebf78b9b7 100644 --- a/packages/core/schematics/utils/BUILD.bazel +++ b/packages/core/schematics/utils/BUILD.bazel @@ -9,5 +9,8 @@ ts_library( "//packages/compiler", "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", + "@npm//@schematics/angular", + "@npm//@types/node", + "@npm//typescript", ], ) diff --git a/packages/core/schematics/utils/project_tsconfig_paths.ts b/packages/core/schematics/utils/project_tsconfig_paths.ts index 05d46ae1cc..6690974fb0 100644 --- a/packages/core/schematics/utils/project_tsconfig_paths.ts +++ b/packages/core/schematics/utils/project_tsconfig_paths.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {normalize} from '@angular-devkit/core'; +import {JsonParseMode, normalize, parseJson} from '@angular-devkit/core'; import {Tree} from '@angular-devkit/schematics'; import {WorkspaceProject} from '@schematics/angular/utility/workspace-models'; @@ -78,8 +78,10 @@ function getWorkspaceConfigGracefully(tree: Tree): any { } try { - return JSON.parse(configBuffer.toString()); - } catch { + // Parse the workspace file as JSON5 which is also supported for CLI + // workspace configurations. + return parseJson(configBuffer.toString(), JsonParseMode.Json5); + } catch (e) { return null; } } diff --git a/packages/core/schematics/utils/tslint/BUILD.bazel b/packages/core/schematics/utils/tslint/BUILD.bazel index 320800a510..cae69eca25 100644 --- a/packages/core/schematics/utils/tslint/BUILD.bazel +++ b/packages/core/schematics/utils/tslint/BUILD.bazel @@ -7,4 +7,5 @@ ts_library( visibility = [ "//packages/core/schematics/migrations/template-var-assignment/google3:__pkg__", ], + deps = ["@npm//typescript"], ) diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index e6a2faf3dd..e8c6f81545 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -15,6 +15,7 @@ import {getCompilerFacade} from './compiler/compiler_facade'; import {Console} from './console'; import {Injectable, InjectionToken, Injector, StaticProvider} from './di'; import {ErrorHandler} from './error_handler'; +import {LOCALE_ID} from './i18n/tokens'; import {Type} from './interface/type'; import {COMPILER_OPTIONS, CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; @@ -25,6 +26,7 @@ import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {assertNgModuleType} from './render3/assert'; import {ComponentFactory as R3ComponentFactory} from './render3/component_ref'; +import {DEFAULT_LOCALE_ID, setLocaleId} from './render3/i18n'; import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref'; import {Testability, TestabilityRegistry} from './testability/testability'; import {isDevMode} from './util/is_dev_mode'; @@ -261,6 +263,9 @@ export class PlatformRef { if (!exceptionHandler) { throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?'); } + // If the `LOCALE_ID` provider is defined at bootstrap we set the value for runtime i18n (ivy) + const localeId = moduleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID); + setLocaleId(localeId); moduleRef.onDestroy(() => remove(this._modules, moduleRef)); ngZone !.runOutsideAngular( () => ngZone !.onError.subscribe( diff --git a/packages/core/src/codegen_private_exports.ts b/packages/core/src/codegen_private_exports.ts index fffbd4f4fa..177d4560ab 100644 --- a/packages/core/src/codegen_private_exports.ts +++ b/packages/core/src/codegen_private_exports.ts @@ -7,5 +7,5 @@ */ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver'; -export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_loader'; +export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_registration'; export {ArgumentType as ɵArgumentType, BindingFlags as ɵBindingFlags, DepFlags as ɵDepFlags, EMPTY_ARRAY as ɵEMPTY_ARRAY, EMPTY_MAP as ɵEMPTY_MAP, NodeFlags as ɵNodeFlags, QueryBindingType as ɵQueryBindingType, QueryValueType as ɵQueryValueType, ViewDefinition as ɵViewDefinition, ViewFlags as ɵViewFlags, anchorDef as ɵand, createComponentFactory as ɵccf, createNgModuleFactory as ɵcmf, createRendererType2 as ɵcrt, directiveDef as ɵdid, elementDef as ɵeld, getComponentViewDefinitionFactory as ɵgetComponentViewDefinitionFactory, inlineInterpolate as ɵinlineInterpolate, interpolate as ɵinterpolate, moduleDef as ɵmod, moduleProvideDef as ɵmpd, ngContentDef as ɵncd, nodeValue as ɵnov, pipeDef as ɵpid, providerDef as ɵprd, pureArrayDef as ɵpad, pureObjectDef as ɵpod, purePipeDef as ɵppd, queryDef as ɵqud, textDef as ɵted, unwrapValue as ɵunv, viewDef as ɵvid} from './view/index'; diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 7214522ddd..8fb8a44d07 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -106,6 +106,7 @@ export interface R3NgModuleMetadataFacade { exports: Function[]; emitInline: boolean; schemas: {name: string}[]|null; + id: string|null; } export interface R3InjectorMetadataFacade { diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 1a446cbee5..510f4326f1 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -14,7 +14,7 @@ export {isListLikeIterable as ɵisListLikeIterable} from './change_detection/cha export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants'; export {Console as ɵConsole} from './console'; export {inject, setCurrentInjector as ɵsetCurrentInjector, ɵɵinject} from './di/injector_compatibility'; -export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDef as ɵɵInjectableDef, ɵɵInjectorDef} from './di/interface/defs'; +export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDef, ɵɵInjectorDef} from './di/interface/defs'; export {APP_ROOT as ɵAPP_ROOT} from './di/scope'; export {ivyEnabled as ɵivyEnabled} from './ivy_switch'; export {ComponentFactory as ɵComponentFactory} from './linker/component_factory'; @@ -34,3 +34,5 @@ export {makeDecorator as ɵmakeDecorator} from './util/decorators'; export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang'; export {clearOverrides as ɵclearOverrides, initServicesIfNeeded as ɵinitServicesIfNeeded, overrideComponentView as ɵoverrideComponentView, overrideProvider as ɵoverrideProvider} from './view/index'; export {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from './view/provider'; +export {getLocalePluralCase as ɵgetLocalePluralCase, findLocaleData as ɵfindLocaleData} from './i18n/locale_data_api'; +export {LOCALE_DATA as ɵLOCALE_DATA, LocaleDataIndex as ɵLocaleDataIndex} from './i18n/locale_data'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index c7374775ac..b23bc2c35f 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -8,6 +8,16 @@ // clang-format off export { + ɵɵattribute, + ɵɵattributeInterpolate1, + ɵɵattributeInterpolate2, + ɵɵattributeInterpolate3, + ɵɵattributeInterpolate4, + ɵɵattributeInterpolate5, + ɵɵattributeInterpolate6, + ɵɵattributeInterpolate7, + ɵɵattributeInterpolate8, + ɵɵattributeInterpolateV, ɵɵdefineBase, ɵɵdefineComponent, ɵɵdefineDirective, @@ -47,6 +57,16 @@ export { ɵɵelement, ɵɵlistener, ɵɵtext, + ɵɵtextInterpolate, + ɵɵtextInterpolate1, + ɵɵtextInterpolate2, + ɵɵtextInterpolate3, + ɵɵtextInterpolate4, + ɵɵtextInterpolate5, + ɵɵtextInterpolate6, + ɵɵtextInterpolate7, + ɵɵtextInterpolate8, + ɵɵtextInterpolateV, ɵɵembeddedViewStart, ɵɵprojection, ɵɵbind, @@ -100,7 +120,7 @@ export { ɵɵpropertyInterpolate7, ɵɵpropertyInterpolate8, ɵɵpropertyInterpolateV, - ɵɵcomponentHostSyntheticProperty, + ɵɵupdateSyntheticHostBinding, ɵɵcomponentHostSyntheticListener, ɵɵprojectionDef, ɵɵreference, @@ -142,6 +162,8 @@ export { ɵɵi18nPostprocess, i18nConfigureLocalize as ɵi18nConfigureLocalize, ɵɵi18nLocalize, + setLocaleId as ɵsetLocaleId, + DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID, setClassMetadata as ɵsetClassMetadata, ɵɵresolveWindow, ɵɵresolveDocument, @@ -274,10 +296,9 @@ export { SWITCH_RENDERER2_FACTORY__POST_R3__ as ɵSWITCH_RENDERER2_FACTORY__POST_R3__, } from './render/api'; -export { - getModuleFactory__POST_R3__ as ɵgetModuleFactory__POST_R3__, - registerNgModuleType as ɵregisterNgModuleType, -} from './linker/ng_module_factory_loader'; +export { getModuleFactory__POST_R3__ as ɵgetModuleFactory__POST_R3__ } from './linker/ng_module_factory_loader'; + +export { registerNgModuleType as ɵregisterNgModuleType } from './linker/ng_module_factory_registration'; export { publishGlobalUtil as ɵpublishGlobalUtil, diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index f4ecc218c2..bc15842731 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -423,24 +423,24 @@ function _queryAllR3( function _queryNodeChildrenR3( tNode: TNode, lView: LView, predicate: Predicate, matches: DebugNode[], elementsOnly: boolean, rootNativeNode: any) { + const nativeNode = getNativeByTNode(tNode, lView); // For each type of TNode, specific logic is executed. if (tNode.type === TNodeType.Element || tNode.type === TNodeType.ElementContainer) { // Case 1: the TNode is an element // The native node has to be checked. - _addQueryMatchR3( - getNativeByTNode(tNode, lView), predicate, matches, elementsOnly, rootNativeNode); + _addQueryMatchR3(nativeNode, predicate, matches, elementsOnly, rootNativeNode); if (isComponent(tNode)) { // If the element is the host of a component, then all nodes in its view have to be processed. // Note: the component's content (tNode.child) will be processed from the insertion points. const componentView = getComponentViewByIndex(tNode.index, lView); - if (componentView && componentView[TVIEW].firstChild) + if (componentView && componentView[TVIEW].firstChild) { _queryNodeChildrenR3( componentView[TVIEW].firstChild !, componentView, predicate, matches, elementsOnly, rootNativeNode); - } else { + } + } else if (tNode.child) { // Otherwise, its children have to be processed. - if (tNode.child) - _queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly, rootNativeNode); + _queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly, rootNativeNode); } // In all cases, if a dynamic container exists for this node, each view inside it has to be // processed. @@ -468,25 +468,24 @@ function _queryNodeChildrenR3( for (let nativeNode of head) { _addQueryMatchR3(nativeNode, predicate, matches, elementsOnly, rootNativeNode); } - } else { - if (head) { - const nextLView = componentView[PARENT] !as LView; - const nextTNode = nextLView[TVIEW].data[head.index] as TNode; - _queryNodeChildrenR3( - nextTNode, nextLView, predicate, matches, elementsOnly, rootNativeNode); - } + } else if (head) { + const nextLView = componentView[PARENT] !as LView; + const nextTNode = nextLView[TVIEW].data[head.index] as TNode; + _queryNodeChildrenR3(nextTNode, nextLView, predicate, matches, elementsOnly, rootNativeNode); } - } else { + } else if (tNode.child) { // Case 4: the TNode is a view. - if (tNode.child) { - _queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly, rootNativeNode); - } + _queryNodeChildrenR3(tNode.child, lView, predicate, matches, elementsOnly, rootNativeNode); } - // To determine the next node to be processed, we need to use the next or the projectionNext link, - // depending on whether the current node has been projected. - const nextTNode = (tNode.flags & TNodeFlags.isProjected) ? tNode.projectionNext : tNode.next; - if (nextTNode) { - _queryNodeChildrenR3(nextTNode, lView, predicate, matches, elementsOnly, rootNativeNode); + + // We don't want to go to the next sibling of the root node. + if (rootNativeNode !== nativeNode) { + // To determine the next node to be processed, we need to use the next or the projectionNext + // link, depending on whether the current node has been projected. + const nextTNode = (tNode.flags & TNodeFlags.isProjected) ? tNode.projectionNext : tNode.next; + if (nextTNode) { + _queryNodeChildrenR3(nextTNode, lView, predicate, matches, elementsOnly, rootNativeNode); + } } } @@ -638,14 +637,19 @@ function getDebugNode__PRE_R3__(nativeNode: any): DebugNode|null { return _nativeNodeToDebugNode.get(nativeNode) || null; } +const NG_DEBUG_PROPERTY = '__ng_debug__'; + export function getDebugNode__POST_R3__(nativeNode: Element): DebugElement__POST_R3__; export function getDebugNode__POST_R3__(nativeNode: Node): DebugNode__POST_R3__; export function getDebugNode__POST_R3__(nativeNode: null): null; export function getDebugNode__POST_R3__(nativeNode: any): DebugNode|null { if (nativeNode instanceof Node) { - return nativeNode.nodeType == Node.ELEMENT_NODE ? - new DebugElement__POST_R3__(nativeNode as Element) : - new DebugNode__POST_R3__(nativeNode); + if (!(nativeNode.hasOwnProperty(NG_DEBUG_PROPERTY))) { + (nativeNode as any)[NG_DEBUG_PROPERTY] = nativeNode.nodeType == Node.ELEMENT_NODE ? + new DebugElement__POST_R3__(nativeNode as Element) : + new DebugNode__POST_R3__(nativeNode); + } + return (nativeNode as any)[NG_DEBUG_PROPERTY]; } return null; } @@ -678,9 +682,9 @@ export interface Predicate { (value: T): boolean; } /** * @publicApi */ -export const DebugNode: {new (...args: any[]): DebugNode} = DebugNode__PRE_R3__ as any; +export const DebugNode: {new (...args: any[]): DebugNode} = DebugNode__PRE_R3__; /** * @publicApi */ -export const DebugElement: {new (...args: any[]): DebugElement} = DebugElement__PRE_R3__ as any; +export const DebugElement: {new (...args: any[]): DebugElement} = DebugElement__PRE_R3__; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index bac0d1fe20..cd6556c18e 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -117,7 +117,6 @@ const enum OptionFlags { CheckParent = 1 << 2, Default = CheckSelf | CheckParent } -const NULL_INJECTOR = Injector.NULL; const NO_NEW_LINE = 'ɵ'; export class StaticInjector implements Injector { @@ -127,7 +126,7 @@ export class StaticInjector implements Injector { private _records: Map; constructor( - providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) { + providers: StaticProvider[], parent: Injector = Injector.NULL, source: string|null = null) { this.parent = parent; this.source = source; const records = this._records = new Map(); @@ -304,7 +303,7 @@ function resolveToken( records, // If we don't know how to resolve dependency and we should not check parent for it, // than pass in Null injector. - !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, + !childRecord && !(options & OptionFlags.CheckParent) ? Injector.NULL : parent, options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND, InjectFlags.Default)); } diff --git a/packages/core/src/di/interface/defs.ts b/packages/core/src/di/interface/defs.ts index dd00972be8..e92f931825 100644 --- a/packages/core/src/di/interface/defs.ts +++ b/packages/core/src/di/interface/defs.ts @@ -129,7 +129,7 @@ export interface InjectorTypeWithProviders { * * `factory` gives the zero argument function which will create an instance of the injectable. * The factory can call `inject` to access the `Injector` and request injection of dependencies. * - * @publicApi + * @codeGenApi */ export function ɵɵdefineInjectable(opts: { providedIn?: Type| 'root' | 'any' | null, @@ -175,12 +175,34 @@ export function ɵɵdefineInjector(options: {factory: () => any, providers?: any } /** - * Read the `ngInjectableDef` type in a way which is immune to accidentally reading inherited value. + * Read the `ngInjectableDef` for `type` in a way which is immune to accidentally reading inherited + * value. * - * @param type type which may have `ngInjectableDef` + * @param type A type which may have its own (non-inherited) `ngInjectableDef`. */ export function getInjectableDef(type: any): ɵɵInjectableDef|null { - return type && type.hasOwnProperty(NG_INJECTABLE_DEF) ? (type as any)[NG_INJECTABLE_DEF] : null; + return type && type.hasOwnProperty(NG_INJECTABLE_DEF) ? type[NG_INJECTABLE_DEF] : null; +} + +/** + * Read the `ngInjectableDef` for `type` or read the `ngInjectableDef` from one of its ancestors. + * + * @param type A type which may have `ngInjectableDef`, via inheritance. + * + * @deprecated Will be removed in v10, where an error will occur in the scenario if we find the + * `ngInjectableDef` on an ancestor only. + */ +export function getInheritedInjectableDef(type: any): ɵɵInjectableDef|null { + if (type && type[NG_INJECTABLE_DEF]) { + // TODO(FW-1307): Re-add ngDevMode when closure can handle it + // ngDevMode && + console.warn( + `DEPRECATED: DI is instantiating a token "${type.name}" that inherits its @Injectable decorator but does not provide one itself.\n` + + `This will become an error in v10. Please add @Injectable() to the "${type.name}" class.`); + return type[NG_INJECTABLE_DEF]; + } else { + return null; + } } /** diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 670c9902c5..276e868117 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import '../util/ng_dev_mode'; + import {OnDestroy} from '../interface/lifecycle_hooks'; import {Type} from '../interface/type'; import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors'; @@ -15,7 +17,7 @@ import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; import {Injector} from './injector'; import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, catchInjectorError, injectArgs, setCurrentInjector, ɵɵinject} from './injector_compatibility'; -import {InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef, ɵɵInjectableDef} from './interface/defs'; +import {InjectorType, InjectorTypeWithProviders, getInheritedInjectableDef, getInjectableDef, getInjectorDef, ɵɵInjectableDef} from './interface/defs'; import {InjectFlags} from './interface/injector'; import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './interface/provider'; import {APP_ROOT} from './scope'; @@ -222,14 +224,20 @@ export class R3Injector { } /** - * Add an `InjectorType` or `InjectorDefTypeWithProviders` and all of its transitive providers + * Add an `InjectorType` or `InjectorTypeWithProviders` and all of its transitive providers * to this injector. + * + * If an `InjectorTypeWithProviders` that declares providers besides the type is specified, + * the function will return "true" to indicate that the providers of the type definition need + * to be processed. This allows us to process providers of injector types after all imports of + * an injector definition are processed. (following View Engine semantics: see FW-1349) */ private processInjectorType( defOrWrappedDef: InjectorType|InjectorTypeWithProviders, - parents: InjectorType[], dedupStack: InjectorType[]) { + parents: InjectorType[], + dedupStack: InjectorType[]): defOrWrappedDef is InjectorTypeWithProviders { defOrWrappedDef = resolveForwardRef(defOrWrappedDef); - if (!defOrWrappedDef) return; + if (!defOrWrappedDef) return false; // Either the defOrWrappedDef is an InjectorType (with ngInjectorDef) or an // InjectorDefTypeWithProviders (aka ModuleWithProviders). Detecting either is a megamorphic @@ -249,8 +257,7 @@ export class R3Injector { (ngModule === undefined) ? (defOrWrappedDef as InjectorType) : ngModule; // Check for circular dependencies. - // TODO(FW-1307): Re-add ngDevMode when closure can handle it - if (parents.indexOf(defType) !== -1) { + if (ngDevMode && parents.indexOf(defType) !== -1) { const defName = stringify(defType); throw new Error( `Circular dependency in DI detected for type ${defName}. Dependency path: ${parents.map(defType => stringify(defType)).join(' > ')} > ${defName}.`); @@ -259,12 +266,6 @@ export class R3Injector { // Check for multiple imports of the same module const isDuplicate = dedupStack.indexOf(defType) !== -1; - // If defOrWrappedType was an InjectorDefTypeWithProviders, then .providers may hold some - // extra providers. - const providers = - (ngModule !== undefined) && (defOrWrappedDef as InjectorTypeWithProviders).providers || - EMPTY_ARRAY; - // Finally, if defOrWrappedType was an `InjectorDefTypeWithProviders`, then the actual // `InjectorDef` is on its `ngModule`. if (ngModule !== undefined) { @@ -273,7 +274,7 @@ export class R3Injector { // If no definition was found, it might be from exports. Remove it. if (def == null) { - return; + return false; } // Track the InjectorType and add a provider for it. @@ -286,18 +287,35 @@ export class R3Injector { if (def.imports != null && !isDuplicate) { // Before processing defType's imports, add it to the set of parents. This way, if it ends // up deeply importing itself, this can be detected. - // TODO(FW-1307): Re-add ngDevMode when closure can handle it - parents.push(defType); + ngDevMode && parents.push(defType); // Add it to the set of dedups. This way we can detect multiple imports of the same module dedupStack.push(defType); + let importTypesWithProviders: (InjectorTypeWithProviders[])|undefined; try { - deepForEach( - def.imports, imported => this.processInjectorType(imported, parents, dedupStack)); + deepForEach(def.imports, imported => { + if (this.processInjectorType(imported, parents, dedupStack)) { + if (importTypesWithProviders === undefined) importTypesWithProviders = []; + // If the processed import is an injector type with providers, we store it in the + // list of import types with providers, so that we can process those afterwards. + importTypesWithProviders.push(imported); + } + }); } finally { // Remove it from the parents set when finished. - // TODO(FW-1307): Re-add ngDevMode when closure can handle it - parents.pop(); + ngDevMode && parents.pop(); + } + + // Imports which are declared with providers (TypeWithProviders) need to be processed + // after all imported modules are processed. This is similar to how View Engine + // processes/merges module imports in the metadata resolver. See: FW-1349. + if (importTypesWithProviders !== undefined) { + for (let i = 0; i < importTypesWithProviders.length; i++) { + const {ngModule, providers} = importTypesWithProviders[i]; + deepForEach( + providers !, + provider => this.processProvider(provider, ngModule, providers || EMPTY_ARRAY)); + } } } @@ -309,9 +327,9 @@ export class R3Injector { defProviders, provider => this.processProvider(provider, injectorType, defProviders)); } - // Finally, include providers from an InjectorDefTypeWithProviders if there was one. - const ngModuleType = (defOrWrappedDef as InjectorTypeWithProviders).ngModule; - deepForEach(providers, provider => this.processProvider(provider, ngModuleType, providers)); + return ( + ngModule !== undefined && + (defOrWrappedDef as InjectorTypeWithProviders).providers !== undefined); } /** @@ -378,25 +396,52 @@ export class R3Injector { } function injectableDefOrInjectorDefFactory(token: Type| InjectionToken): () => any { - const injectableDef = getInjectableDef(token as InjectableType); - if (injectableDef === null) { - const injectorDef = getInjectorDef(token as InjectorType); - if (injectorDef !== null) { - return injectorDef.factory; - } else if (token instanceof InjectionToken) { - throw new Error(`Token ${stringify(token)} is missing an ngInjectableDef definition.`); - } else if (token instanceof Function) { - const paramLength = token.length; - if (paramLength > 0) { - const args: string[] = new Array(paramLength).fill('?'); - throw new Error( - `Can't resolve all parameters for ${stringify(token)}: (${args.join(', ')}).`); - } - return () => new (token as Type)(); - } - throw new Error('unreachable'); + // Most tokens will have an ngInjectableDef directly on them, which specifies a factory directly. + const injectableDef = getInjectableDef(token); + if (injectableDef !== null) { + return injectableDef.factory; + } + + // If the token is an NgModule, it's also injectable but the factory is on its ngInjectorDef. + const injectorDef = getInjectorDef(token); + if (injectorDef !== null) { + return injectorDef.factory; + } + + // InjectionTokens should have an ngInjectableDef and thus should be handled above. + // If it's missing that, it's an error. + if (token instanceof InjectionToken) { + throw new Error(`Token ${stringify(token)} is missing an ngInjectableDef definition.`); + } + + // Undecorated types can sometimes be created if they have no constructor arguments. + if (token instanceof Function) { + return getUndecoratedInjectableFactory(token); + } + + // There was no way to resolve a factory for this token. + throw new Error('unreachable'); +} + +function getUndecoratedInjectableFactory(token: Function) { + // If the token has parameters then it has dependencies that we cannot resolve implicitly. + const paramLength = token.length; + if (paramLength > 0) { + const args: string[] = new Array(paramLength).fill('?'); + throw new Error(`Can't resolve all parameters for ${stringify(token)}: (${args.join(', ')}).`); + } + + // The constructor function appears to have no parameters. + // This might be because it inherits from a super-class. In which case, use an ngInjectableDef + // from an ancestor if there is one. + // Otherwise this really is a simple class with no dependencies, so return a factory that + // just instantiates the zero-arg constructor. + const inheritedInjectableDef = getInheritedInjectableDef(token); + if (inheritedInjectableDef !== null) { + return inheritedInjectableDef.factory; + } else { + return () => new (token as Type)(); } - return injectableDef.factory; } function providerToRecord( diff --git a/packages/core/src/i18n/locale_data.ts b/packages/core/src/i18n/locale_data.ts new file mode 100644 index 0000000000..734cc16680 --- /dev/null +++ b/packages/core/src/i18n/locale_data.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. 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 + */ + +/** + * This const is used to store the locale data registered with `registerLocaleData` + */ +export const LOCALE_DATA: {[localeId: string]: any} = {}; + +/** + * Index of each type of locale data from the locale data array + */ +export enum LocaleDataIndex { + LocaleId = 0, + DayPeriodsFormat, + DayPeriodsStandalone, + DaysFormat, + DaysStandalone, + MonthsFormat, + MonthsStandalone, + Eras, + FirstDayOfWeek, + WeekendRange, + DateFormat, + TimeFormat, + DateTimeFormat, + NumberSymbols, + NumberFormats, + CurrencySymbol, + CurrencyName, + Currencies, + PluralCase, + ExtraData +} diff --git a/packages/core/src/i18n/locale_data_api.ts b/packages/core/src/i18n/locale_data_api.ts new file mode 100644 index 0000000000..cc30c20348 --- /dev/null +++ b/packages/core/src/i18n/locale_data_api.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright Google Inc. 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 {LOCALE_DATA, LocaleDataIndex} from './locale_data'; +import localeEn from './locale_en'; + +/** + * Retrieves the plural function used by ICU expressions to determine the plural case to use + * for a given locale. + * @param locale A locale code for the locale format rules to use. + * @returns The plural function for the locale. + * @see `NgPlural` + * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) + */ +export function getLocalePluralCase(locale: string): (value: number) => number { + const data = findLocaleData(locale); + return data[LocaleDataIndex.PluralCase]; +} + +/** + * Finds the locale data for a given locale. + * + * @param locale The locale code. + * @returns The locale data. + * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) + */ +export function findLocaleData(locale: string): any { + const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); + + let match = LOCALE_DATA[normalizedLocale]; + if (match) { + return match; + } + + // let's try to find a parent locale + const parentLocale = normalizedLocale.split('-')[0]; + match = LOCALE_DATA[parentLocale]; + + if (match) { + return match; + } + + if (parentLocale === 'en') { + return localeEn; + } + + throw new Error(`Missing locale data for the locale "${locale}".`); +} diff --git a/packages/common/src/i18n/locale_en.ts b/packages/core/src/i18n/locale_en.ts similarity index 100% rename from packages/common/src/i18n/locale_en.ts rename to packages/core/src/i18n/locale_en.ts diff --git a/packages/core/src/i18n/localization.ts b/packages/core/src/i18n/localization.ts new file mode 100644 index 0000000000..0cb492f18b --- /dev/null +++ b/packages/core/src/i18n/localization.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. 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 {getLocalePluralCase} from './locale_data_api'; + +/** + * Returns the plural case based on the locale + */ +export function getPluralCase(value: any, locale: string): string { + const plural = getLocalePluralCase(locale)(value); + + switch (plural) { + case 0: + return 'zero'; + case 1: + return 'one'; + case 2: + return 'two'; + case 3: + return 'few'; + case 4: + return 'many'; + default: + return 'other'; + } +} diff --git a/packages/core/src/linker/ng_module_factory_loader.ts b/packages/core/src/linker/ng_module_factory_loader.ts index f5a8b9aec6..2f6f710c6e 100644 --- a/packages/core/src/linker/ng_module_factory_loader.ts +++ b/packages/core/src/linker/ng_module_factory_loader.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '../interface/type'; import {NgModuleFactory as R3NgModuleFactory, NgModuleType} from '../render3/ng_module_ref'; -import {stringify} from '../util/stringify'; import {NgModuleFactory} from './ng_module_factory'; +import {getRegisteredNgModuleType} from './ng_module_factory_registration'; /** @@ -24,48 +23,14 @@ export abstract class NgModuleFactoryLoader { abstract load(path: string): Promise>; } -/** - * Map of module-id to the corresponding NgModule. - * - In pre Ivy we track NgModuleFactory, - * - In post Ivy we track the NgModuleType - */ -const modules = new Map|NgModuleType>(); - -/** - * Registers a loaded module. Should only be called from generated NgModuleFactory code. - * @publicApi - */ -export function registerModuleFactory(id: string, factory: NgModuleFactory) { - const existing = modules.get(id) as NgModuleFactory; - assertSameOrNotExisting(id, existing && existing.moduleType, factory.moduleType); - modules.set(id, factory); -} - -function assertSameOrNotExisting(id: string, type: Type| null, incoming: Type): void { - if (type && type !== incoming) { - throw new Error( - `Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`); - } -} - -export function registerNgModuleType(id: string, ngModuleType: NgModuleType) { - const existing = modules.get(id) as NgModuleType | null; - assertSameOrNotExisting(id, existing, ngModuleType); - modules.set(id, ngModuleType); -} - -export function clearModulesForTest(): void { - modules.clear(); -} - export function getModuleFactory__PRE_R3__(id: string): NgModuleFactory { - const factory = modules.get(id) as NgModuleFactory| null; + const factory = getRegisteredNgModuleType(id) as NgModuleFactory| null; if (!factory) throw noModuleError(id); return factory; } export function getModuleFactory__POST_R3__(id: string): NgModuleFactory { - const type = modules.get(id) as NgModuleType | null; + const type = getRegisteredNgModuleType(id) as NgModuleType | null; if (!type) throw noModuleError(id); return new R3NgModuleFactory(type); } diff --git a/packages/core/src/linker/ng_module_factory_registration.ts b/packages/core/src/linker/ng_module_factory_registration.ts new file mode 100644 index 0000000000..60ed6d603d --- /dev/null +++ b/packages/core/src/linker/ng_module_factory_registration.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. 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 {Type} from '../interface/type'; +import {NgModuleType} from '../render3/ng_module_ref'; +import {stringify} from '../util/stringify'; + +import {NgModuleFactory} from './ng_module_factory'; + + +/** + * Map of module-id to the corresponding NgModule. + * - In pre Ivy we track NgModuleFactory, + * - In post Ivy we track the NgModuleType + */ +const modules = new Map|NgModuleType>(); + +/** + * Registers a loaded module. Should only be called from generated NgModuleFactory code. + * @publicApi + */ +export function registerModuleFactory(id: string, factory: NgModuleFactory) { + const existing = modules.get(id) as NgModuleFactory; + assertSameOrNotExisting(id, existing && existing.moduleType, factory.moduleType); + modules.set(id, factory); +} + +function assertSameOrNotExisting(id: string, type: Type| null, incoming: Type): void { + if (type && type !== incoming) { + throw new Error( + `Duplicate module registered for ${id} - ${stringify(type)} vs ${stringify(type.name)}`); + } +} + +export function registerNgModuleType(ngModuleType: NgModuleType) { + if (ngModuleType.ngModuleDef.id !== null) { + const id = ngModuleType.ngModuleDef.id; + const existing = modules.get(id) as NgModuleType | null; + assertSameOrNotExisting(id, existing, ngModuleType); + modules.set(id, ngModuleType); + } + + let imports = ngModuleType.ngModuleDef.imports; + if (imports instanceof Function) { + imports = imports(); + } + if (imports) { + imports.forEach((i: NgModuleType) => registerNgModuleType(i)); + } +} + +export function clearModulesForTest(): void { + modules.clear(); +} + +export function getRegisteredNgModuleType(id: string) { + return modules.get(id); +} diff --git a/packages/core/src/linker/query_list.ts b/packages/core/src/linker/query_list.ts index 69bbbf41b7..edc4863804 100644 --- a/packages/core/src/linker/query_list.ts +++ b/packages/core/src/linker/query_list.ts @@ -12,6 +12,9 @@ import {EventEmitter} from '../event_emitter'; import {flatten} from '../util/array_utils'; import {getSymbolIterator} from '../util/symbol'; +function symbolIterator(this: QueryList): Iterator { + return ((this as any as{_results: Array})._results as any)[getSymbolIterator()](); +} /** * An unmodifiable list of items that Angular keeps up to date when the state @@ -63,6 +66,16 @@ export class QueryList/* implements Iterable */ { // TODO(issue/24571): remove '!'. readonly last !: T; + constructor() { + // This function should be declared on the prototype, but doing so there will cause the class + // declaration to have side-effects and become not tree-shakable. For this reason we do it in + // the constructor. + // [getSymbolIterator()](): Iterator { ... } + const symbol = getSymbolIterator(); + const proto = QueryList.prototype as any; + if (!proto[symbol]) proto[symbol] = symbolIterator; + } + /** * See * [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) @@ -124,8 +137,6 @@ export class QueryList/* implements Iterable */ { */ toArray(): T[] { return this._results.slice(); } - [getSymbolIterator()](): Iterator { return (this._results as any)[getSymbolIterator()](); } - toString(): string { return this._results.toString(); } /** diff --git a/packages/core/src/metadata/di.ts b/packages/core/src/metadata/di.ts index e8692e6126..dd6bcb6c04 100644 --- a/packages/core/src/metadata/di.ts +++ b/packages/core/src/metadata/di.ts @@ -102,7 +102,7 @@ export interface Query { read: any; isViewQuery: boolean; selector: any; - static?: boolean; + static: boolean; } /** @@ -218,8 +218,8 @@ export interface ContentChildDecorator { * * @Annotation */ - (selector: Type|Function|string, opts?: {read?: any, static?: boolean}): any; - new (selector: Type|Function|string, opts?: {read?: any, static?: boolean}): ContentChild; + (selector: Type|Function|string, opts: {read?: any, static: boolean}): any; + new (selector: Type|Function|string, opts: {read?: any, static: boolean}): ContentChild; } /** @@ -388,8 +388,8 @@ export interface ViewChildDecorator { * * @Annotation */ - (selector: Type|Function|string, opts?: {read?: any, static?: boolean}): any; - new (selector: Type|Function|string, opts?: {read?: any, static?: boolean}): ViewChild; + (selector: Type|Function|string, opts: {read?: any, static: boolean}): any; + new (selector: Type|Function|string, opts: {read?: any, static: boolean}): ViewChild; } /** diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index d2d6ad56e8..d8e6fb95ff 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -105,6 +105,9 @@ export interface NgModuleDef { /** The set of schemas that declare elements to be allowed in the NgModule. */ schemas: SchemaMetadata[]|null; + + /** Unique ID for the module with which it should be registered. */ + id: string|null; } /** diff --git a/packages/core/src/metadata/resource_loading.ts b/packages/core/src/metadata/resource_loading.ts index ce4d7be738..c9ae16a81b 100644 --- a/packages/core/src/metadata/resource_loading.ts +++ b/packages/core/src/metadata/resource_loading.ts @@ -103,7 +103,7 @@ export function isComponentDefPendingResolution(type: Type): boolean { export function componentNeedsResolution(component: Component): boolean { return !!( - (component.templateUrl && !component.template) || + (component.templateUrl && !component.hasOwnProperty('template')) || component.styleUrls && component.styleUrls.length); } export function clearResolutionOfComponentResourcesQueue(): Map, Component> { diff --git a/packages/core/src/profile/profile.ts b/packages/core/src/profile/profile.ts index 07a774c133..70563b59f1 100644 --- a/packages/core/src/profile/profile.ts +++ b/packages/core/src/profile/profile.ts @@ -49,6 +49,7 @@ function noopScope(arg0?: any, arg1?: any): any { * an exception is expected during normal execution while profiling. * * @publicApi + * @deprecated the Web Tracing Framework is no longer supported in Angular */ export const wtfCreateScope: (signature: string, flags?: any) => WtfScopeFn = wtfEnabled ? createScope : (signature: string, flags?: any) => noopScope; @@ -61,6 +62,7 @@ export const wtfCreateScope: (signature: string, flags?: any) => WtfScopeFn = * * Returns the `returnValue for easy chaining. * @publicApi + * @deprecated the Web Tracing Framework is no longer supported in Angular */ export const wtfLeave: (scope: any, returnValue?: T) => T = wtfEnabled ? leave : (s: any, r?: any) => r; @@ -77,6 +79,7 @@ export const wtfLeave: (scope: any, returnValue?: T) => T = * }); * } * @publicApi + * @deprecated the Web Tracing Framework is no longer supported in Angular */ export const wtfStartTimeRange: (rangeType: string, action: string) => any = wtfEnabled ? startTimeRange : (rangeType: string, action: string) => null; @@ -86,5 +89,6 @@ export const wtfStartTimeRange: (rangeType: string, action: string) => any = * [range] is the return value from [wtfStartTimeRange] Async ranges only work if WTF has been * enabled. * @publicApi + * @deprecated the Web Tracing Framework is no longer supported in Angular */ export const wtfEndTimeRange: (range: any) => void = wtfEnabled ? endTimeRange : (r: any) => null; diff --git a/packages/core/src/profile/wtf_impl.ts b/packages/core/src/profile/wtf_impl.ts index ca2972d13c..ce2720d08a 100644 --- a/packages/core/src/profile/wtf_impl.ts +++ b/packages/core/src/profile/wtf_impl.ts @@ -12,6 +12,7 @@ import {global} from '../util/global'; * A scope function for the Web Tracing Framework (WTF). * * @publicApi + * @deprecated the Web Tracing Framework is no longer supported in Angular */ export interface WtfScopeFn { (arg0?: any, arg1?: any): any; } diff --git a/packages/core/src/render3/PERF_NOTES.md b/packages/core/src/render3/PERF_NOTES.md index fc94cb2266..625b37a2ac 100644 --- a/packages/core/src/render3/PERF_NOTES.md +++ b/packages/core/src/render3/PERF_NOTES.md @@ -104,8 +104,18 @@ export function i18nStartFirstTemplatePass(tView: TView, index: number, message: } ``` - - ## Loops Don't use `forEach`, it can cause megamorphic function calls (depending on the browser) and function allocations. It is [a lot slower than regular `for` loops](https://jsperf.com/for-vs-foreach-misko) + +## Limit global state access + +Ivy implementation uses some variables in `packages/core/src/render3/state.ts` that could be considered "global state" (those are not truly global variables exposed on `window` but still those variables are easily accessible from anywhere in the ivy codebase). Usage of this global state should be limited to avoid unnecessary function calls (state getters) and improve code readability. + +As a rule, the global state should be accessed _only_ from instructions (functions invoked from the generated code). + +## Instructions should be only called from the generated code + +Instruction functions should be called only from the generated template code. As a consequence of this rule, instructions shouldn't call other instructions. + +Calling instructions from other instructions (or any part of the ivy codebase) multiplies global state access (see previous rule) and makes reasoning about code more difficult. \ No newline at end of file diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index 8dc9d23802..483b8d2ccd 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -36,8 +36,9 @@ export function assertPreviousIsParent(isParent: boolean) { assertEqual(isParent, true, 'previousOrParentTNode should be a parent'); } -export function assertHasParent(tNode: TNode) { - assertDefined(tNode.parent, 'previousOrParentTNode should have a parent'); +export function assertHasParent(tNode: TNode | null) { + assertDefined(tNode, 'previousOrParentTNode should exist!'); + assertDefined(tNode !.parent, 'previousOrParentTNode should have a parent'); } export function assertDataNext(lView: LView, index: number, arr?: any[]) { diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index ea31239937..ed6cd447e7 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -11,12 +11,13 @@ import {Type} from '../core'; import {Injector} from '../di/injector'; import {Sanitizer} from '../sanitization/security'; +import {assertDataInRange, assertEqual} from '../util/assert'; import {assertComponentType} from './assert'; import {getComponentDef} from './definition'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {registerPostOrderHooks, registerPreOrderHooks} from './hooks'; -import {CLEAN_PROMISE, addToViewTree, createLView, createNodeAtIndex, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions/shared'; +import {CLEAN_PROMISE, addToViewTree, createLView, createTView, getOrCreateTNode, getOrCreateTView, initNodeFlags, instantiateRootComponent, invokeHostBindingsInCreationMode, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions/shared'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; @@ -60,7 +61,7 @@ export interface CreateComponentOptions { * Typically, the features in this list are features that cannot be added to the * other features list in the component definition because they rely on other factors. * - * Example: `RootLifecycleHooks` is a function that adds lifecycle hook capabilities + * Example: `LifecycleHooksFeature` is a function that adds lifecycle hook capabilities * to root components in a tree-shakable way. It cannot be added to the component * features list because there's no way of knowing when the component will be used as * a root component. @@ -172,13 +173,12 @@ export function createRootComponentView( rendererFactory: RendererFactory3, renderer: Renderer3, sanitizer?: Sanitizer | null): LView { resetComponentState(); const tView = rootView[TVIEW]; - const tNode: TElementNode = createNodeAtIndex(0, TNodeType.Element, rNode, null, null); + ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET); + rootView[0 + HEADER_OFFSET] = rNode; + const tNode: TElementNode = getOrCreateTNode(tView, null, 0, TNodeType.Element, null, null); const componentView = createLView( - rootView, getOrCreateTView( - def.template, def.consts, def.vars, def.directiveDefs, def.pipeDefs, - def.viewQuery, def.schemas), - null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode, - rendererFactory, renderer, sanitizer); + rootView, getOrCreateTView(def), null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, + rootView[HEADER_OFFSET], tNode, rendererFactory, renderer, sanitizer); if (tView.firstTemplatePass) { diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), rootView, def.type); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 0141e69868..4a9000058d 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -129,10 +129,8 @@ export class ComponentFactory extends viewEngine_ComponentFactory { super(); this.componentType = componentDef.type; this.selector = componentDef.selectors[0][0] as string; - // The component definition does not include the wildcard ('*') selector in its list. - // It is implicitly expected as the first item in the projectable nodes array. this.ngContentSelectors = - componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : []; + componentDef.ngContentSelectors ? componentDef.ngContentSelectors : []; this.isBoundToModule = !!ngModule; } diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index ecefde30c9..ed07f6a001 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -7,7 +7,6 @@ */ import '../util/ng_dev_mode'; - import {ChangeDetectionStrategy} from '../change_detection/constants'; import {NG_INJECTABLE_DEF, ɵɵdefineInjectable} from '../di/interface/defs'; import {Mutable, Type} from '../interface/type'; @@ -16,9 +15,8 @@ import {SchemaMetadata} from '../metadata/schema'; import {ViewEncapsulation} from '../metadata/view'; import {noSideEffects} from '../util/closure'; import {stringify} from '../util/stringify'; - import {EMPTY_ARRAY, EMPTY_OBJ} from './empty'; -import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; +import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_LOCALE_ID_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields'; import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction, ɵɵBaseDef} from './interfaces/definition'; // while SelectorFlags is unused here, it's required so that types don't get resolved lazily // see: https://github.com/Microsoft/web-build-tools/issues/1050 @@ -285,6 +283,7 @@ export function ɵɵdefineComponent(componentDefinition: { _: null as never, setInput: null, schemas: componentDefinition.schemas || null, + tView: null, }; def._ = noSideEffects(() => { const directiveTypes = componentDefinition.directives !; @@ -366,6 +365,9 @@ export function ɵɵdefineNgModule(def: { /** The set of schemas that declare elements to be allowed in the NgModule. */ schemas?: SchemaMetadata[] | null; + + /** Unique ID for the module that is used with `getModuleFactory`. */ + id?: string | null; }): never { const res: NgModuleDef = { type: def.type, @@ -375,6 +377,7 @@ export function ɵɵdefineNgModule(def: { exports: def.exports || EMPTY_ARRAY, transitiveCompileScopes: null, schemas: def.schemas || null, + id: def.id || null, }; return res as never; } @@ -774,3 +777,7 @@ export function getNgModuleDef(type: any, throwNotFound?: boolean): NgModuleD } return ngModuleDef; } + +export function getNgLocaleIdDef(type: any): string|null { + return (type as any)[NG_LOCALE_ID_DEF] || null; +} diff --git a/packages/core/src/render3/empty.ts b/packages/core/src/render3/empty.ts index a3870a64cd..d0494193fd 100644 --- a/packages/core/src/render3/empty.ts +++ b/packages/core/src/render3/empty.ts @@ -19,6 +19,10 @@ export const EMPTY_ARRAY: any[] = []; // freezing the values prevents any code from accidentally inserting new values in if (typeof ngDevMode !== 'undefined' && ngDevMode) { + // These property accesses can be ignored because ngDevMode will be set to false + // when optimizing code and the whole if statement will be dropped. + // tslint:disable-next-line:no-toplevel-property-access Object.freeze(EMPTY_OBJ); + // tslint:disable-next-line:no-toplevel-property-access Object.freeze(EMPTY_ARRAY); } diff --git a/packages/core/src/render3/fields.ts b/packages/core/src/render3/fields.ts index 26b29af9da..92642b417f 100644 --- a/packages/core/src/render3/fields.ts +++ b/packages/core/src/render3/fields.ts @@ -12,6 +12,7 @@ export const NG_COMPONENT_DEF = getClosureSafeProperty({ngComponentDef: getClosu export const NG_DIRECTIVE_DEF = getClosureSafeProperty({ngDirectiveDef: getClosureSafeProperty}); export const NG_PIPE_DEF = getClosureSafeProperty({ngPipeDef: getClosureSafeProperty}); export const NG_MODULE_DEF = getClosureSafeProperty({ngModuleDef: getClosureSafeProperty}); +export const NG_LOCALE_ID_DEF = getClosureSafeProperty({ngLocaleIdDef: getClosureSafeProperty}); export const NG_BASE_DEF = getClosureSafeProperty({ngBaseDef: getClosureSafeProperty}); /** diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 26e7eae042..e3291dcfa8 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -8,16 +8,17 @@ import '../util/ng_i18n_closure_mode'; +import {getPluralCase} from '../i18n/localization'; import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; import {InertBodyHelper} from '../sanitization/inert_body'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {addAllToArray} from '../util/array_utils'; -import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; +import {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; import {attachPatchData} from './context_discovery'; -import {attachI18nOpCodesDebug} from './debug'; -import {ɵɵelementAttribute, ɵɵload, ɵɵtextBinding} from './instructions/all'; -import {allocExpando, createNodeAtIndex} from './instructions/shared'; +import {elementAttributeInternal, ɵɵload, ɵɵtextBinding} from './instructions/all'; +import {attachI18nOpCodesDebug} from './instructions/lview_debug'; +import {allocExpando, elementPropertyInternal, getOrCreateTNode, setInputsForProperty} from './instructions/shared'; import {LContainer, NATIVE} from './interfaces/container'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n'; import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node'; @@ -26,11 +27,12 @@ import {SanitizerFn} from './interfaces/sanitization'; import {StylingContext} from './interfaces/styling'; import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from './interfaces/view'; import {appendChild, createTextNode, nativeRemoveNode} from './node_manipulation'; -import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from './state'; +import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from './state'; import {NO_CHANGE} from './tokens'; import {renderStringify} from './util/misc_utils'; import {getNativeByIndex, getNativeByTNode, getTNode, isLContainer} from './util/view_utils'; + const MARKER = `�`; const ICU_BLOCK_REGEXP = /^\s*(�\d+:?\d*�)\s*,\s*(select|plural)\s*,/; const SUBTEMPLATE_REGEXP = /�\/?\*(\d+:\d+)�/gi; @@ -121,7 +123,7 @@ function extractParts(pattern: string): (string | IcuExpression)[] { const block = pattern.substring(prevPos, pos); if (ICU_BLOCK_REGEXP.test(block)) { results.push(parseICUBlock(block)); - } else if (block) { // Don't push empty strings + } else { results.push(block); } @@ -138,10 +140,7 @@ function extractParts(pattern: string): (string | IcuExpression)[] { } const substring = pattern.substring(prevPos); - if (substring != '') { - results.push(substring); - } - + results.push(substring); return results; } @@ -180,7 +179,7 @@ function parseICUBlock(pattern: string): IcuExpression { } const blocks = extractParts(parts[pos++]) as string[]; - if (blocks.length) { + if (cases.length > values.length) { values.push(blocks); } } @@ -556,8 +555,8 @@ export function ɵɵi18nPostprocess( const templateIdsStack: number[] = [ROOT_TEMPLATE_ID]; result = result.replace(PP_PLACEHOLDERS_REGEXP, (m: any, phs: string, tmpl: string): string => { const content = phs || tmpl; - if (!matches[content]) { - const placeholders: PostprocessPlaceholder[] = []; + const placeholders: PostprocessPlaceholder[] = matches[content] || []; + if (!placeholders.length) { content.split('|').forEach((placeholder: string) => { const match = placeholder.match(PP_TEMPLATE_ID_REGEXP); const templateId = match ? parseInt(match[1], 10) : ROOT_TEMPLATE_ID; @@ -566,11 +565,12 @@ export function ɵɵi18nPostprocess( }); matches[content] = placeholders; } - if (!matches[content].length) { + + if (!placeholders.length) { throw new Error(`i18n postprocess: unmatched placeholder - ${content}`); } + const currentTemplateId = templateIdsStack[templateIdsStack.length - 1]; - const placeholders = matches[content]; let idx = 0; // find placeholder index that matches current template id for (let i = 0; i < placeholders.length; i++) { @@ -590,12 +590,6 @@ export function ɵɵi18nPostprocess( placeholders.splice(idx, 1); return placeholder; }); - - // verify that we injected all values - const hasUnmatchedValues = Object.keys(matches).some(key => !!matches[key].length); - if (hasUnmatchedValues) { - throw new Error(`i18n postprocess: unmatched values - ${JSON.stringify(matches)}`); - } } // return current result if no replacements specified @@ -671,10 +665,12 @@ function i18nEndFirstPass(tView: TView) { * Creates and stores the dynamic TNode, and unhooks it from the tree for now. */ function createDynamicNodeAtIndex( - index: number, type: TNodeType, native: RElement | RText | null, + lView: LView, index: number, type: TNodeType, native: RElement | RText | null, name: string | null): TElementNode|TIcuContainerNode { const previousOrParentTNode = getPreviousOrParentTNode(); - const tNode = createNodeAtIndex(index, type as any, native, name, null); + ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); + lView[index + HEADER_OFFSET] = native; + const tNode = getOrCreateTNode(lView[TVIEW], lView[T_HOST], index, type as any, name, null); // We are creating a dynamic node, the previous tNode might not be pointing at this node. // We will link ourselves into the tree later with `appendI18nNode`. @@ -699,9 +695,10 @@ function readCreateOpCodes( const textNodeIndex = createOpCodes[++i] as number; ngDevMode && ngDevMode.rendererCreateTextNode++; previousTNode = currentTNode; - currentTNode = createDynamicNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null); + currentTNode = + createDynamicNodeAtIndex(viewData, textNodeIndex, TNodeType.Element, textRNode, null); visitedNodes.push(textNodeIndex); - setIsParent(false); + setIsNotParent(); } else if (typeof opCode == 'number') { switch (opCode & I18nMutateOpCode.MASK_OPCODE) { case I18nMutateOpCode.AppendChild: @@ -726,23 +723,22 @@ function readCreateOpCodes( previousTNode = currentTNode; currentTNode = getTNode(nodeIndex, viewData); if (currentTNode) { - setPreviousOrParentTNode(currentTNode); - if (currentTNode.type === TNodeType.Element) { - setIsParent(true); - } + setPreviousOrParentTNode(currentTNode, currentTNode.type === TNodeType.Element); } break; case I18nMutateOpCode.ElementEnd: const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; previousTNode = currentTNode = getTNode(elementIndex, viewData); - setPreviousOrParentTNode(currentTNode); - setIsParent(false); + setPreviousOrParentTNode(currentTNode, false); break; case I18nMutateOpCode.Attr: const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; const attrName = createOpCodes[++i] as string; const attrValue = createOpCodes[++i] as string; - ɵɵelementAttribute(elementNodeIndex, attrName, attrValue); + const renderer = viewData[RENDERER]; + // This code is used for ICU expressions only, since we don't support + // directives/components in ICUs, we don't need to worry about inputs here + elementAttributeInternal(elementNodeIndex, attrName, attrValue, viewData, renderer); break; default: throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); @@ -759,12 +755,12 @@ function readCreateOpCodes( ngDevMode && ngDevMode.rendererCreateComment++; previousTNode = currentTNode; currentTNode = createDynamicNodeAtIndex( - commentNodeIndex, TNodeType.IcuContainer, commentRNode, null); + viewData, commentNodeIndex, TNodeType.IcuContainer, commentRNode, null); visitedNodes.push(commentNodeIndex); attachPatchData(commentRNode, viewData); (currentTNode as TIcuContainerNode).activeCaseIndex = null; // We will add the case nodes later, during the update phase - setIsParent(false); + setIsNotParent(); break; case ELEMENT_MARKER: const tagNameValue = createOpCodes[++i] as string; @@ -776,7 +772,7 @@ function readCreateOpCodes( ngDevMode && ngDevMode.rendererCreateElement++; previousTNode = currentTNode; currentTNode = createDynamicNodeAtIndex( - elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue); + viewData, elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue); visitedNodes.push(elementNodeIndex); break; default: @@ -785,7 +781,7 @@ function readCreateOpCodes( } } - setIsParent(false); + setIsNotParent(); return visitedNodes; } @@ -817,9 +813,9 @@ function readUpdateOpCodes( let icuTNode: TIcuContainerNode; switch (opCode & I18nUpdateOpCode.MASK_OPCODE) { case I18nUpdateOpCode.Attr: - const attrName = updateOpCodes[++j] as string; + const propName = updateOpCodes[++j] as string; const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null; - ɵɵelementAttribute(nodeIndex, attrName, value, sanitizeFn); + elementPropertyInternal(nodeIndex, propName, value, sanitizeFn); break; case I18nUpdateOpCode.Text: ɵɵtextBinding(nodeIndex, value); @@ -961,6 +957,7 @@ function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) if (j & 1) { // Odd indexes are ICU expressions // TODO(ocombe): support ICU expressions in attributes + throw new Error('ICU expressions are not yet supported in attributes'); } else if (value !== '') { // Even indexes are text (including bindings) const hasBinding = !!value.match(BINDING_REGEXP); @@ -968,7 +965,15 @@ function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) addAllToArray( generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes); } else { - ɵɵelementAttribute(previousElementIndex, attrName, value); + const lView = getLView(); + const renderer = lView[RENDERER]; + elementAttributeInternal(previousElementIndex, attrName, value, lView, renderer); + // Check if that attribute is a directive input + const tNode = getTNode(previousElementIndex, lView); + const dataValue = tNode.inputs && tNode.inputs[attrName]; + if (dataValue) { + setInputsForProperty(lView, dataValue, value); + } } } } @@ -1026,351 +1031,6 @@ export function ɵɵi18nApply(index: number) { } } -enum Plural { - Zero = 0, - One = 1, - Two = 2, - Few = 3, - Many = 4, - Other = 5, -} - -/** - * Returns the plural case based on the locale. - * This is a copy of the deprecated function that we used in Angular v4. - * // TODO(ocombe): remove this once we can the real getPluralCase function - * - * @deprecated from v5 the plural case function is in locale data files common/locales/*.ts - */ -function getPluralCase(locale: string, nLike: number | string): Plural { - if (typeof nLike === 'string') { - nLike = parseInt(nLike, 10); - } - const n: number = nLike as number; - const nDecimal = n.toString().replace(/^[^.]*\.?/, ''); - const i = Math.floor(Math.abs(n)); - const v = nDecimal.length; - const f = parseInt(nDecimal, 10); - const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0; - - const lang = locale.split('-')[0].toLowerCase(); - - switch (lang) { - case 'af': - case 'asa': - case 'az': - case 'bem': - case 'bez': - case 'bg': - case 'brx': - case 'ce': - case 'cgg': - case 'chr': - case 'ckb': - case 'ee': - case 'el': - case 'eo': - case 'es': - case 'eu': - case 'fo': - case 'fur': - case 'gsw': - case 'ha': - case 'haw': - case 'hu': - case 'jgo': - case 'jmc': - case 'ka': - case 'kk': - case 'kkj': - case 'kl': - case 'ks': - case 'ksb': - case 'ky': - case 'lb': - case 'lg': - case 'mas': - case 'mgo': - case 'ml': - case 'mn': - case 'nb': - case 'nd': - case 'ne': - case 'nn': - case 'nnh': - case 'nyn': - case 'om': - case 'or': - case 'os': - case 'ps': - case 'rm': - case 'rof': - case 'rwk': - case 'saq': - case 'seh': - case 'sn': - case 'so': - case 'sq': - case 'ta': - case 'te': - case 'teo': - case 'tk': - case 'tr': - case 'ug': - case 'uz': - case 'vo': - case 'vun': - case 'wae': - case 'xog': - if (n === 1) return Plural.One; - return Plural.Other; - case 'ak': - case 'ln': - case 'mg': - case 'pa': - case 'ti': - if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One; - return Plural.Other; - case 'am': - case 'as': - case 'bn': - case 'fa': - case 'gu': - case 'hi': - case 'kn': - case 'mr': - case 'zu': - if (i === 0 || n === 1) return Plural.One; - return Plural.Other; - case 'ar': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many; - return Plural.Other; - case 'ast': - case 'ca': - case 'de': - case 'en': - case 'et': - case 'fi': - case 'fy': - case 'gl': - case 'it': - case 'nl': - case 'sv': - case 'sw': - case 'ur': - case 'yi': - if (i === 1 && v === 0) return Plural.One; - return Plural.Other; - case 'be': - if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One; - if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 && - !(n % 100 >= 12 && n % 100 <= 14)) - return Plural.Few; - if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 || - n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'br': - if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One; - if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two; - if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) && - !(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 || - n % 100 >= 90 && n % 100 <= 99)) - return Plural.Few; - if (!(n === 0) && n % 1e6 === 0) return Plural.Many; - return Plural.Other; - case 'bs': - case 'hr': - case 'sr': - if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11)) - return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14) || - f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 && - !(f % 100 >= 12 && f % 100 <= 14)) - return Plural.Few; - return Plural.Other; - case 'cs': - case 'sk': - if (i === 1 && v === 0) return Plural.One; - if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few; - if (!(v === 0)) return Plural.Many; - return Plural.Other; - case 'cy': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n === 3) return Plural.Few; - if (n === 6) return Plural.Many; - return Plural.Other; - case 'da': - if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One; - return Plural.Other; - case 'dsb': - case 'hsb': - if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One; - if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two; - if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || - f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4) - return Plural.Few; - return Plural.Other; - case 'ff': - case 'fr': - case 'hy': - case 'kab': - if (i === 0 || i === 1) return Plural.One; - return Plural.Other; - case 'fil': - if (v === 0 && (i === 1 || i === 2 || i === 3) || - v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) || - !(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9)) - return Plural.One; - return Plural.Other; - case 'ga': - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few; - if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many; - return Plural.Other; - case 'gd': - if (n === 1 || n === 11) return Plural.One; - if (n === 2 || n === 12) return Plural.Two; - if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few; - return Plural.Other; - case 'gv': - if (v === 0 && i % 10 === 1) return Plural.One; - if (v === 0 && i % 10 === 2) return Plural.Two; - if (v === 0 && - (i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80)) - return Plural.Few; - if (!(v === 0)) return Plural.Many; - return Plural.Other; - case 'he': - if (i === 1 && v === 0) return Plural.One; - if (i === 2 && v === 0) return Plural.Two; - if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many; - return Plural.Other; - case 'is': - if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One; - return Plural.Other; - case 'ksh': - if (n === 0) return Plural.Zero; - if (n === 1) return Plural.One; - return Plural.Other; - case 'kw': - case 'naq': - case 'se': - case 'smn': - if (n === 1) return Plural.One; - if (n === 2) return Plural.Two; - return Plural.Other; - case 'lag': - if (n === 0) return Plural.Zero; - if ((i === 0 || i === 1) && !(n === 0)) return Plural.One; - return Plural.Other; - case 'lt': - if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One; - if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 && - !(n % 100 >= 11 && n % 100 <= 19)) - return Plural.Few; - if (!(f === 0)) return Plural.Many; - return Plural.Other; - case 'lv': - case 'prg': - if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 || - v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19) - return Plural.Zero; - if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) || - !(v === 2) && f % 10 === 1) - return Plural.One; - return Plural.Other; - case 'mk': - if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One; - return Plural.Other; - case 'mt': - if (n === 1) return Plural.One; - if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10) - return Plural.Few; - if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many; - return Plural.Other; - case 'pl': - if (i === 1 && v === 0) return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14)) - return Plural.Few; - if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 || - v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || - v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'pt': - if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One; - return Plural.Other; - case 'ro': - if (i === 1 && v === 0) return Plural.One; - if (!(v === 0) || n === 0 || - !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19) - return Plural.Few; - return Plural.Other; - case 'ru': - case 'uk': - if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One; - if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && - !(i % 100 >= 12 && i % 100 <= 14)) - return Plural.Few; - if (v === 0 && i % 10 === 0 || - v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || - v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14) - return Plural.Many; - return Plural.Other; - case 'shi': - if (i === 0 || n === 1) return Plural.One; - if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few; - return Plural.Other; - case 'si': - if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One; - return Plural.Other; - case 'sl': - if (v === 0 && i % 100 === 1) return Plural.One; - if (v === 0 && i % 100 === 2) return Plural.Two; - if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0)) - return Plural.Few; - return Plural.Other; - case 'tzm': - if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99) - return Plural.One; - return Plural.Other; - // When there is no specification, the default is always "other" - // Spec: http://cldr.unicode.org/index/cldr-spec/plural-rules - // > other (required—general plural form — also used if the language only has a single form) - default: - return Plural.Other; - } -} - -function getPluralCategory(value: any, locale: string): string { - const plural = getPluralCase(locale, value); - - switch (plural) { - case Plural.Zero: - return 'zero'; - case Plural.One: - return 'one'; - case Plural.Two: - return 'two'; - case Plural.Few: - return 'few'; - case Plural.Many: - return 'many'; - default: - return 'other'; - } -} - /** * Returns the index of the current case of an ICU expression depending on the main binding value * @@ -1382,9 +1042,7 @@ function getCaseIndex(icuExpression: TIcu, bindingValue: string): number { if (index === -1) { switch (icuExpression.type) { case IcuType.plural: { - // TODO(ocombe): replace this hard-coded value by the real LOCALE_ID value - const locale = 'en-US'; - const resolvedCase = getPluralCategory(bindingValue, locale); + const resolvedCase = getPluralCase(bindingValue, getLocaleId()); index = icuExpression.cases.indexOf(resolvedCase); if (index === -1 && resolvedCase !== 'other') { index = icuExpression.cases.indexOf('other'); @@ -1630,7 +1288,7 @@ const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g; * running outside of Closure Compiler. This method will not be needed once runtime translation * service support is introduced. * - * @publicApi + * @codeGenApi * @deprecated this method is temporary & should not be used as it will be removed soon */ export function ɵɵi18nLocalize(input: string, placeholders: {[key: string]: string} = {}) { @@ -1641,3 +1299,31 @@ export function ɵɵi18nLocalize(input: string, placeholders: {[key: string]: st input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') : input; } + +/** + * The locale id that the application is currently using (for translations and ICU expressions). + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + */ +export const DEFAULT_LOCALE_ID = 'en-US'; +let LOCALE_ID = DEFAULT_LOCALE_ID; + +/** + * Sets the locale id that will be used for translations and ICU expressions. + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + * + * @param localeId + */ +export function setLocaleId(localeId: string) { + LOCALE_ID = localeId.toLowerCase().replace(/_/g, '-'); +} + +/** + * Gets the locale id that will be used for translations and ICU expressions. + * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine + * but is now defined as a global value. + */ +export function getLocaleId(): string { + return LOCALE_ID; +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 34c39ec47f..723982608a 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -21,12 +21,24 @@ export { markDirty, store, tick, + ɵɵallocHostVars, + + ɵɵattribute, + ɵɵattributeInterpolate1, + ɵɵattributeInterpolate2, + ɵɵattributeInterpolate3, + ɵɵattributeInterpolate4, + ɵɵattributeInterpolate5, + ɵɵattributeInterpolate6, + ɵɵattributeInterpolate7, + ɵɵattributeInterpolate8, + ɵɵattributeInterpolateV, + ɵɵbind, ɵɵclassMap, ɵɵclassProp, ɵɵcomponentHostSyntheticListener, - ɵɵcomponentHostSyntheticProperty, ɵɵcontainer, ɵɵcontainerRefreshEnd, @@ -89,14 +101,28 @@ export { ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, + ɵɵstyleSanitizer, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, - ɵɵtextBinding} from './instructions/all'; + ɵɵtextBinding, + ɵɵtextInterpolate, + ɵɵtextInterpolate1, + ɵɵtextInterpolate2, + ɵɵtextInterpolate3, + ɵɵtextInterpolate4, + ɵɵtextInterpolate5, + ɵɵtextInterpolate6, + ɵɵtextInterpolate7, + ɵɵtextInterpolate8, + ɵɵtextInterpolateV, + + ɵɵupdateSyntheticHostBinding, +} from './instructions/all'; export {RenderFlags} from './interfaces/definition'; -export {CssSelectorList} from './interfaces/projection'; +export {CssSelectorList, ProjectionSlots} from './interfaces/projection'; export { ɵɵrestoreView, @@ -106,6 +132,7 @@ export { } from './state'; export { + DEFAULT_LOCALE_ID, ɵɵi18n, ɵɵi18nAttributes, ɵɵi18nExp, @@ -115,6 +142,8 @@ export { ɵɵi18nPostprocess, i18nConfigureLocalize, ɵɵi18nLocalize, + getLocaleId, + setLocaleId, } from './i18n'; export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index faee257e01..ab291b1cb4 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -26,6 +26,8 @@ * Jira Issue = FW-1184 */ export * from './alloc_host_vars'; +export * from './attribute'; +export * from './attribute_interpolation'; export * from './change_detection'; export * from './container'; export * from './storage'; @@ -34,6 +36,7 @@ export * from './element'; export * from './element_container'; export * from './embedded_view'; export * from './get_current_view'; +export * from './interpolation'; export * from './listener'; export * from './namespace'; export * from './next_context'; @@ -42,4 +45,6 @@ export * from './property'; export * from './property_interpolation'; export * from './select'; export * from './styling'; +export {styleSanitizer as ɵɵstyleSanitizer} from '../styling_next/instructions'; export * from './text'; +export * from './text_interpolation'; diff --git a/packages/core/src/render3/instructions/attribute.ts b/packages/core/src/render3/instructions/attribute.ts new file mode 100644 index 0000000000..f5fdc13f8a --- /dev/null +++ b/packages/core/src/render3/instructions/attribute.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright Google Inc. 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 {SanitizerFn} from '../interfaces/sanitization'; +import {getSelectedIndex} from '../state'; + +import {ɵɵelementAttribute} from './element'; +import {ɵɵbind} from './property'; + +/** + * Updates the value of or removes a bound attribute on an Element. + * + * Used in the case of `[attr.title]="value"` + * + * @param name name The name of the attribute. + * @param value value The attribute is removed when value is `null` or `undefined`. + * Otherwise the attribute value is set to the stringified value. + * @param sanitizer An optional function used to sanitize the value. + * @param namespace Optional namespace to use when setting the attribute. + * + * @codeGenApi + */ +export function ɵɵattribute( + name: string, value: any, sanitizer?: SanitizerFn | null, namespace?: string) { + const index = getSelectedIndex(); + // TODO(FW-1340): Refactor to remove the use of other instructions here. + return ɵɵelementAttribute(index, name, ɵɵbind(value), sanitizer, namespace); +} diff --git a/packages/core/src/render3/instructions/attribute_interpolation.ts b/packages/core/src/render3/instructions/attribute_interpolation.ts new file mode 100644 index 0000000000..c0505532e9 --- /dev/null +++ b/packages/core/src/render3/instructions/attribute_interpolation.ts @@ -0,0 +1,397 @@ +/** + * @license + * Copyright Google Inc. 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 {SanitizerFn} from '../interfaces/sanitization'; +import {getSelectedIndex} from '../state'; +import {ɵɵelementAttribute} from './element'; +import {ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolation4, ɵɵinterpolation5, ɵɵinterpolation6, ɵɵinterpolation7, ɵɵinterpolation8, ɵɵinterpolationV} from './interpolation'; +import {TsickleIssue1009} from './shared'; + + + +/** + * + * Update an interpolated attribute on an element with single bound value surrounded by text. + * + * Used when the value passed to a property has 1 interpolated value in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate1('title', 'prefix', v0, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate1( + attrName: string, prefix: string, v0: any, suffix: string, sanitizer?: SanitizerFn, + namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = ɵɵinterpolation1(prefix, v0, suffix); + + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + + return ɵɵattributeInterpolate1; +} + +/** + * + * Update an interpolated attribute on an element with 2 bound values surrounded by text. + * + * Used when the value passed to a property has 2 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate2('title', 'prefix', v0, '-', v1, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate2( + attrName: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, + sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = ɵɵinterpolation2(prefix, v0, i0, v1, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate2; +} + +/** + * + * Update an interpolated attribute on an element with 3 bound values surrounded by text. + * + * Used when the value passed to a property has 3 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate3( + * 'title', 'prefix', v0, '-', v1, '-', v2, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate3( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, + suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = ɵɵinterpolation3(prefix, v0, i0, v1, i1, v2, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate3; +} + +/** + * + * Update an interpolated attribute on an element with 4 bound values surrounded by text. + * + * Used when the value passed to a property has 4 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate4( + * 'title', 'prefix', v0, '-', v1, '-', v2, '-', v3, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param i2 Static value used for concatenation only. + * @param v3 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate4( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = ɵɵinterpolation4(prefix, v0, i0, v1, i1, v2, i2, v3, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate4; +} + +/** + * + * Update an interpolated attribute on an element with 5 bound values surrounded by text. + * + * Used when the value passed to a property has 5 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate5( + * 'title', 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param i2 Static value used for concatenation only. + * @param v3 Value checked for change. + * @param i3 Static value used for concatenation only. + * @param v4 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate5( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, suffix: string, sanitizer?: SanitizerFn, + namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = ɵɵinterpolation5(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate5; +} + +/** + * + * Update an interpolated attribute on an element with 6 bound values surrounded by text. + * + * Used when the value passed to a property has 6 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate6( + * 'title', 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param i2 Static value used for concatenation only. + * @param v3 Value checked for change. + * @param i3 Static value used for concatenation only. + * @param v4 Value checked for change. + * @param i4 Static value used for concatenation only. + * @param v5 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate6( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, sanitizer?: SanitizerFn, + namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = + ɵɵinterpolation6(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate6; +} + +/** + * + * Update an interpolated attribute on an element with 7 bound values surrounded by text. + * + * Used when the value passed to a property has 7 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate7( + * 'title', 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param i2 Static value used for concatenation only. + * @param v3 Value checked for change. + * @param i3 Static value used for concatenation only. + * @param v4 Value checked for change. + * @param i4 Static value used for concatenation only. + * @param v5 Value checked for change. + * @param i5 Static value used for concatenation only. + * @param v6 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate7( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, + sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = + ɵɵinterpolation7(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate7; +} + +/** + * + * Update an interpolated attribute on an element with 8 bound values surrounded by text. + * + * Used when the value passed to a property has 8 interpolated values in it: + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolate8( + * 'title', 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, '-', v7, 'suffix'); + * ``` + * + * @param attrName The name of the attribute to update + * @param prefix Static value used for concatenation only. + * @param v0 Value checked for change. + * @param i0 Static value used for concatenation only. + * @param v1 Value checked for change. + * @param i1 Static value used for concatenation only. + * @param v2 Value checked for change. + * @param i2 Static value used for concatenation only. + * @param v3 Value checked for change. + * @param i3 Static value used for concatenation only. + * @param v4 Value checked for change. + * @param i4 Static value used for concatenation only. + * @param v5 Value checked for change. + * @param i5 Static value used for concatenation only. + * @param v6 Value checked for change. + * @param i6 Static value used for concatenation only. + * @param v7 Value checked for change. + * @param suffix Static value used for concatenation only. + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolate8( + attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, + v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, + suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + // TODO(FW-1340): Refactor to remove the use of other instructions here. + const interpolatedValue = + ɵɵinterpolation8(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); + ɵɵelementAttribute(index, attrName, interpolatedValue, sanitizer, namespace); + return ɵɵattributeInterpolate8; +} + +/** + * Update an interpolated attribute on an element with 8 or more bound values surrounded by text. + * + * Used when the number of interpolated values exceeds 7. + * + * ```html + *
+ * ``` + * + * Its compiled representation is:: + * + * ```ts + * ɵɵattributeInterpolateV( + * 'title', ['prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, '-', v7, '-', v9, + * 'suffix']); + * ``` + * + * @param attrName The name of the attribute to update. + * @param values The a collection of values and the strings in-between those values, beginning with + * a string prefix and ending with a string suffix. + * (e.g. `['prefix', value0, '-', value1, '-', value2, ..., value99, 'suffix']`) + * @param sanitizer An optional sanitizer function + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵattributeInterpolateV( + attrName: string, values: any[], sanitizer?: SanitizerFn, + namespace?: string): TsickleIssue1009 { + const index = getSelectedIndex(); + // TODO(FW-1340): Refactor to remove the use of other instructions here. + ɵɵelementAttribute(index, attrName, ɵɵinterpolationV(values), sanitizer, namespace); + return ɵɵattributeInterpolateV; +} diff --git a/packages/core/src/render3/instructions/container.ts b/packages/core/src/render3/instructions/container.ts index f5909b2947..c3869b2698 100644 --- a/packages/core/src/render3/instructions/container.ts +++ b/packages/core/src/render3/instructions/container.ts @@ -5,19 +5,21 @@ * 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 {assertEqual} from '../../util/assert'; +import {assertDataInRange, assertEqual} from '../../util/assert'; import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {executePreOrderHooks, registerPostOrderHooks} from '../hooks'; import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container'; import {ComponentTemplate} from '../interfaces/definition'; import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType} from '../interfaces/node'; -import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild, removeView} from '../node_manipulation'; -import {getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state'; +import {getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; import {getNativeByTNode, loadInternal} from '../util/view_utils'; -import {addToViewTree, createDirectivesAndLocals, createLContainer, createNodeAtIndex, createTView} from './shared'; + +import {addToViewTree, createDirectivesAndLocals, createLContainer, createTView, getOrCreateTNode} from './shared'; + /** * Creates an LContainer for inline views, e.g. @@ -37,7 +39,7 @@ export function ɵɵcontainer(index: number): void { tNode.tViews = []; } addTContainerToQueries(lView, tNode); - setIsParent(false); + setIsNotParent(); } /** @@ -77,7 +79,7 @@ export function ɵɵtemplate( addTContainerToQueries(lView, tContainerNode); attachPatchData(getNativeByTNode(tContainerNode, lView), lView); registerPostOrderHooks(tView, tContainerNode); - setIsParent(false); + setIsNotParent(); } /** @@ -91,10 +93,8 @@ export function ɵɵcontainerRefreshStart(index: number): void { const lView = getLView(); const tView = lView[TVIEW]; let previousOrParentTNode = loadInternal(tView.data, index) as TNode; - setPreviousOrParentTNode(previousOrParentTNode); - ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container); - setIsParent(true); + setPreviousOrParentTNode(previousOrParentTNode, true); lView[index + HEADER_OFFSET][ACTIVE_INDEX] = 0; @@ -113,12 +113,12 @@ export function ɵɵcontainerRefreshStart(index: number): void { export function ɵɵcontainerRefreshEnd(): void { let previousOrParentTNode = getPreviousOrParentTNode(); if (getIsParent()) { - setIsParent(false); + setIsNotParent(); } else { ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.View); ngDevMode && assertHasParent(previousOrParentTNode); previousOrParentTNode = previousOrParentTNode.parent !; - setPreviousOrParentTNode(previousOrParentTNode); + setPreviousOrParentTNode(previousOrParentTNode, false); } ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container); @@ -166,9 +166,12 @@ function containerInternal( 'container nodes should be created before any bindings'); const adjustedIndex = index + HEADER_OFFSET; - const comment = lView[RENDERER].createComment(ngDevMode ? 'container' : ''); + ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); ngDevMode && ngDevMode.rendererCreateComment++; - const tNode = createNodeAtIndex(index, TNodeType.Container, comment, tagName, attrs); + const comment = lView[index + HEADER_OFFSET] = + lView[RENDERER].createComment(ngDevMode ? 'container' : ''); + const tNode = + getOrCreateTNode(lView[TVIEW], lView[T_HOST], index, TNodeType.Container, tagName, attrs); const lContainer = lView[adjustedIndex] = createLContainer(lView[adjustedIndex], lView, comment, tNode); diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 4a095580d2..1cdc428251 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -6,26 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ import {validateAgainstEventAttributes} from '../../sanitization/sanitization'; -import {assertDataInRange, assertEqual} from '../../util/assert'; +import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; import {TAttributes, TNodeFlags, TNodeType} from '../interfaces/node'; -import {RElement, isProceduralRenderer} from '../interfaces/renderer'; +import {RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; import {SanitizerFn} from '../interfaces/sanitization'; import {StylingContext} from '../interfaces/styling'; -import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, LView, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; -import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state'; +import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings'; import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util'; +import {registerInitialStylingIntoContext} from '../styling_next/instructions'; +import {runtimeIsNewStylingInUse} from '../styling_next/state'; import {NO_CHANGE} from '../tokens'; import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils'; import {renderStringify} from '../util/misc_utils'; import {getNativeByIndex, getNativeByTNode, getTNode} from '../util/view_utils'; -import {createDirectivesAndLocals, createNodeAtIndex, elementCreate, executeContentQueries, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; +import {createDirectivesAndLocals, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, setInputsForProperty, setNodeStylingTemplate} from './shared'; import {getActiveDirectiveStylingIndex} from './styling'; @@ -53,18 +55,17 @@ export function ɵɵelementStart( 'elements should be created before any bindings '); ngDevMode && ngDevMode.rendererCreateElement++; - - const native = elementCreate(name); + ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); + const native = lView[index + HEADER_OFFSET] = elementCreate(name); const renderer = lView[RENDERER]; - - ngDevMode && assertDataInRange(lView, index - 1); - - const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null); + const tNode = + getOrCreateTNode(tView, lView[T_HOST], index, TNodeType.Element, name, attrs || null); let initialStylesIndex = 0; let initialClassesIndex = 0; + let lastAttrIndex = -1; if (attrs) { - const lastAttrIndex = setUpAttributes(native, attrs); + lastAttrIndex = setUpAttributes(native, attrs); // it's important to only prepare styling-related datastructures once for a given // tNode and not each time an element is created. Also, the styling code is designed @@ -75,12 +76,13 @@ export function ɵɵelementStart( // instantiated into a context per element) setNodeStylingTemplate(tView, tNode, attrs, lastAttrIndex); - if (tNode.stylingTemplate) { + const stylingTemplate = tNode.stylingTemplate; + if (stylingTemplate) { // the initial style/class values are rendered immediately after having been // initialized into the context so the element styling is ready when directives // are initialized (since they may read style/class values in their constructor) - initialStylesIndex = renderInitialStyles(native, tNode.stylingTemplate, renderer); - initialClassesIndex = renderInitialClasses(native, tNode.stylingTemplate, renderer); + initialStylesIndex = renderInitialStyles(native, stylingTemplate, renderer); + initialClassesIndex = renderInitialClasses(native, stylingTemplate, renderer); } } @@ -116,10 +118,14 @@ export function ɵɵelementStart( renderInitialStyles(native, tNode.stylingTemplate, renderer, initialStylesIndex); } + if (runtimeIsNewStylingInUse() && lastAttrIndex >= 0) { + registerInitialStylingIntoContext(tNode, attrs as TAttributes, lastAttrIndex); + } + const currentQueries = lView[QUERIES]; if (currentQueries) { currentQueries.addNode(tNode); - lView[QUERIES] = currentQueries.clone(); + lView[QUERIES] = currentQueries.clone(tNode); } executeContentQueries(tView, tNode, lView); } @@ -131,12 +137,13 @@ export function ɵɵelementStart( */ export function ɵɵelementEnd(): void { let previousOrParentTNode = getPreviousOrParentTNode(); + ngDevMode && assertDefined(previousOrParentTNode, 'No parent node to close.'); if (getIsParent()) { - setIsParent(false); + setIsNotParent(); } else { ngDevMode && assertHasParent(getPreviousOrParentTNode()); previousOrParentTNode = previousOrParentTNode.parent !; - setPreviousOrParentTNode(previousOrParentTNode); + setPreviousOrParentTNode(previousOrParentTNode, false); } // this is required for all host-level styling-related instructions to run @@ -146,7 +153,8 @@ export function ɵɵelementEnd(): void { ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element); const lView = getLView(); const currentQueries = lView[QUERIES]; - if (currentQueries) { + // Go back up to parent queries only if queries have been cloned on this element. + if (currentQueries && previousOrParentTNode.index === currentQueries.nodeIndex) { lView[QUERIES] = currentQueries.parent; } @@ -191,9 +199,9 @@ export function ɵɵelement( /** - * Updates the value of removes an attribute on an Element. + * Updates the value or removes an attribute on an Element. * - * @param number index The index of the element in the data array + * @param index The index of the element in the data array * @param name name The name of the attribute. * @param value value The attribute is removed when value is `null` or `undefined`. * Otherwise the attribute value is set to the stringified value. @@ -206,27 +214,33 @@ export function ɵɵelementAttribute( index: number, name: string, value: any, sanitizer?: SanitizerFn | null, namespace?: string): void { if (value !== NO_CHANGE) { - ngDevMode && validateAgainstEventAttributes(name); const lView = getLView(); const renderer = lView[RENDERER]; - const element = getNativeByIndex(index, lView) as RElement; - if (value == null) { - ngDevMode && ngDevMode.rendererRemoveAttribute++; - isProceduralRenderer(renderer) ? renderer.removeAttribute(element, name, namespace) : - element.removeAttribute(name); + elementAttributeInternal(index, name, value, lView, renderer, sanitizer, namespace); + } +} + +export function elementAttributeInternal( + index: number, name: string, value: any, lView: LView, renderer: Renderer3, + sanitizer?: SanitizerFn | null, namespace?: string) { + ngDevMode && validateAgainstEventAttributes(name); + const element = getNativeByIndex(index, lView) as RElement; + if (value == null) { + ngDevMode && ngDevMode.rendererRemoveAttribute++; + isProceduralRenderer(renderer) ? renderer.removeAttribute(element, name, namespace) : + element.removeAttribute(name); + } else { + ngDevMode && ngDevMode.rendererSetAttribute++; + const tNode = getTNode(index, lView); + const strValue = + sanitizer == null ? renderStringify(value) : sanitizer(value, tNode.tagName || '', name); + + + if (isProceduralRenderer(renderer)) { + renderer.setAttribute(element, name, strValue, namespace); } else { - ngDevMode && ngDevMode.rendererSetAttribute++; - const tNode = getTNode(index, lView); - const strValue = - sanitizer == null ? renderStringify(value) : sanitizer(value, tNode.tagName || '', name); - - - if (isProceduralRenderer(renderer)) { - renderer.setAttribute(element, name, strValue, namespace); - } else { - namespace ? element.setAttributeNS(namespace, name, strValue) : - element.setAttribute(name, strValue); - } + namespace ? element.setAttributeNS(namespace, name, strValue) : + element.setAttribute(name, strValue); } } } diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 4ad34536f2..456051db21 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -10,12 +10,14 @@ import {assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; import {TAttributes, TNodeType} from '../interfaces/node'; -import {BINDING_INDEX, QUERIES, RENDERER, TVIEW} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, QUERIES, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {applyOnCreateInstructions} from '../node_util'; -import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from '../state'; -import {createDirectivesAndLocals, createNodeAtIndex, executeContentQueries, setNodeStylingTemplate} from './shared'; +import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; + +import {createDirectivesAndLocals, executeContentQueries, getOrCreateTNode, setNodeStylingTemplate} from './shared'; + /** * Creates a logical container for other nodes () backed by a comment node in the DOM. @@ -42,11 +44,12 @@ export function ɵɵelementContainerStart( 'element containers should be created before any bindings'); ngDevMode && ngDevMode.rendererCreateComment++; - const native = renderer.createComment(ngDevMode ? tagName : ''); + ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); + const native = lView[index + HEADER_OFFSET] = renderer.createComment(ngDevMode ? tagName : ''); ngDevMode && assertDataInRange(lView, index - 1); - const tNode = - createNodeAtIndex(index, TNodeType.ElementContainer, native, tagName, attrs || null); + const tNode = getOrCreateTNode( + tView, lView[T_HOST], index, TNodeType.ElementContainer, tagName, attrs || null); if (attrs) { @@ -62,7 +65,7 @@ export function ɵɵelementContainerStart( const currentQueries = lView[QUERIES]; if (currentQueries) { currentQueries.addNode(tNode); - lView[QUERIES] = currentQueries.clone(); + lView[QUERIES] = currentQueries.clone(tNode); } executeContentQueries(tView, tNode, lView); } @@ -77,16 +80,17 @@ export function ɵɵelementContainerEnd(): void { const lView = getLView(); const tView = lView[TVIEW]; if (getIsParent()) { - setIsParent(false); + setIsNotParent(); } else { ngDevMode && assertHasParent(previousOrParentTNode); previousOrParentTNode = previousOrParentTNode.parent !; - setPreviousOrParentTNode(previousOrParentTNode); + setPreviousOrParentTNode(previousOrParentTNode, false); } ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.ElementContainer); const currentQueries = lView[QUERIES]; - if (currentQueries) { + // Go back up to parent queries only if queries have been cloned on this element. + if (currentQueries && previousOrParentTNode.index === currentQueries.nodeIndex) { lView[QUERIES] = currentQueries.parent; } diff --git a/packages/core/src/render3/instructions/embedded_view.ts b/packages/core/src/render3/instructions/embedded_view.ts index e2e5ea31f8..e805c59143 100644 --- a/packages/core/src/render3/instructions/embedded_view.ts +++ b/packages/core/src/render3/instructions/embedded_view.ts @@ -40,7 +40,7 @@ export function ɵɵembeddedViewStart( let viewToRender = scanForView(lContainer, lContainer[ACTIVE_INDEX] !, viewBlockId); if (viewToRender) { - setIsParent(true); + setIsParent(); enterView(viewToRender, viewToRender[TVIEW].node); } else { // When we create a new LView, we always reset the state of the instructions. @@ -141,6 +141,5 @@ export function ɵɵembeddedViewEnd(): void { const lContainer = lView[PARENT] as LContainer; ngDevMode && assertLContainerOrUndefined(lContainer); leaveView(lContainer[PARENT] !); - setPreviousOrParentTNode(viewHost !); - setIsParent(false); + setPreviousOrParentTNode(viewHost !, false); } diff --git a/packages/core/src/render3/instructions/interpolation.ts b/packages/core/src/render3/instructions/interpolation.ts new file mode 100644 index 0000000000..284bb5c143 --- /dev/null +++ b/packages/core/src/render3/instructions/interpolation.ts @@ -0,0 +1,289 @@ +/** + * @license + * Copyright Google Inc. 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 {assertEqual, assertLessThan} from '../../util/assert'; +import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from '../bindings'; +import {BINDING_INDEX, TVIEW} from '../interfaces/view'; +import {getLView} from '../state'; +import {NO_CHANGE} from '../tokens'; +import {renderStringify} from '../util/misc_utils'; + +import {storeBindingMetadata} from './shared'; + + + +/** + * Create interpolation bindings with a variable number of expressions. + * + * If there are 1 to 8 expressions `interpolation1()` to `interpolation8()` should be used instead. + * Those are faster because there is no need to create an array of expressions and iterate over it. + * + * `values`: + * - has static text at even indexes, + * - has evaluated expressions at odd indexes. + * + * Returns the concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + * + * @codeGenApi + */ +export function ɵɵinterpolationV(values: any[]): string|NO_CHANGE { + ngDevMode && assertLessThan(2, values.length, 'should have at least 3 values'); + ngDevMode && assertEqual(values.length % 2, 1, 'should have an odd number of values'); + let isBindingUpdated = false; + const lView = getLView(); + const tData = lView[TVIEW].data; + let bindingIndex = lView[BINDING_INDEX]; + + if (tData[bindingIndex] == null) { + // 2 is the index of the first static interstitial value (ie. not prefix) + for (let i = 2; i < values.length; i += 2) { + tData[bindingIndex++] = values[i]; + } + bindingIndex = lView[BINDING_INDEX]; + } + + for (let i = 1; i < values.length; i += 2) { + // Check if bindings (odd indexes) have changed + isBindingUpdated = bindingUpdated(lView, bindingIndex++, values[i]) || isBindingUpdated; + } + lView[BINDING_INDEX] = bindingIndex; + storeBindingMetadata(lView, values[0], values[values.length - 1]); + + if (!isBindingUpdated) { + return NO_CHANGE; + } + + // Build the updated content + let content = values[0]; + for (let i = 1; i < values.length; i += 2) { + content += renderStringify(values[i]) + values[i + 1]; + } + + return content; +} + +/** + * Creates an interpolation binding with 1 expression. + * + * @param prefix static value used for concatenation only. + * @param v0 value checked for change. + * @param suffix static value used for concatenation only. + * + * @codeGenApi + */ +export function ɵɵinterpolation1(prefix: string, v0: any, suffix: string): string|NO_CHANGE { + const lView = getLView(); + const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0); + storeBindingMetadata(lView, prefix, suffix); + return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; +} + +/** + * Creates an interpolation binding with 2 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation2( + prefix: string, v0: any, i0: string, v1: any, suffix: string): string|NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated2(lView, bindingIndex, v0, v1); + lView[BINDING_INDEX] += 2; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + lView[TVIEW].data[bindingIndex] = i0; + } + + return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + suffix : NO_CHANGE; +} + +/** + * Creates an interpolation binding with 3 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation3( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string| + NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2); + lView[BINDING_INDEX] += 3; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 4 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation4( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + suffix: string): string|NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); + lView[BINDING_INDEX] += 4; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + + renderStringify(v3) + suffix : + NO_CHANGE; +} + +/** + * Creates an interpolation binding with 5 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation5( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, suffix: string): string|NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); + different = bindingUpdated(lView, bindingIndex + 4, v4) || different; + lView[BINDING_INDEX] += 5; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + + renderStringify(v3) + i3 + renderStringify(v4) + suffix : + NO_CHANGE; +} + +/** + * Creates an interpolation binding with 6 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation6( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, suffix: string): string|NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); + different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different; + lView[BINDING_INDEX] += 6; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + suffix : + NO_CHANGE; +} + +/** + * Creates an interpolation binding with 7 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation7( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string): string| + NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); + different = bindingUpdated3(lView, bindingIndex + 4, v4, v5, v6) || different; + lView[BINDING_INDEX] += 7; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + tData[bindingIndex + 5] = i5; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + + renderStringify(v6) + suffix : + NO_CHANGE; +} + +/** + * Creates an interpolation binding with 8 expressions. + * + * @codeGenApi + */ +export function ɵɵinterpolation8( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, + suffix: string): string|NO_CHANGE { + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]; + let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); + different = bindingUpdated4(lView, bindingIndex + 4, v4, v5, v6, v7) || different; + lView[BINDING_INDEX] += 8; + + // Only set static strings the first time (data will be null subsequent runs). + const data = storeBindingMetadata(lView, prefix, suffix); + if (data) { + const tData = lView[TVIEW].data; + tData[bindingIndex] = i0; + tData[bindingIndex + 1] = i1; + tData[bindingIndex + 2] = i2; + tData[bindingIndex + 3] = i3; + tData[bindingIndex + 4] = i4; + tData[bindingIndex + 5] = i5; + tData[bindingIndex + 6] = i6; + } + + return different ? + prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + + renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + + renderStringify(v6) + i6 + renderStringify(v7) + suffix : + NO_CHANGE; +} diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index ee61c22680..a9d50aae1f 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -9,6 +9,7 @@ import {assertDataInRange} from '../../util/assert'; import {isObservable} from '../../util/lang'; +import {EMPTY_OBJ} from '../empty'; import {PropertyAliasValue, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {GlobalTargetResolver, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; import {CLEANUP, FLAGS, LView, LViewFlags, RENDERER, TVIEW} from '../interfaces/view'; @@ -115,7 +116,7 @@ function listenerInternal( // add native event listener - applicable to elements only if (tNode.type === TNodeType.Element) { const native = getNativeByTNode(tNode, lView) as RElement; - const resolved = eventTargetResolver ? eventTargetResolver(native) : {} as any; + const resolved = eventTargetResolver ? eventTargetResolver(native) : EMPTY_OBJ as any; const target = resolved.target || native; const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; const lCleanup = getCleanup(lView); diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/instructions/lview_debug.ts similarity index 72% rename from packages/core/src/render3/debug.ts rename to packages/core/src/render3/instructions/lview_debug.ts index f0cf7258ed..f82ca663a7 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -6,20 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import {assertDefined} from '../util/assert'; +import {ComponentTemplate} from '..'; +import {SchemaMetadata} from '../../core'; +import {assertDefined} from '../../util/assert'; +import {createNamedArrayType} from '../../util/named_array_type'; +import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from '../interfaces/container'; +import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n'; +import {TElementNode, TNode, TViewNode} from '../interfaces/node'; +import {LQueries} from '../interfaces/query'; +import {RComment, RElement} from '../interfaces/renderer'; +import {StylingContext} from '../interfaces/styling'; +import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, T_HOST} from '../interfaces/view'; +import {runtimeIsNewStylingInUse} from '../styling_next/state'; +import {DebugStyling as DebugNewStyling, NodeStylingDebug} from '../styling_next/styling_debug'; +import {attachDebugObject} from '../util/debug_utils'; +import {getTNode, isStylingContext, unwrapRNode} from '../util/view_utils'; -import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from './interfaces/container'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from './interfaces/i18n'; -import {TNode} from './interfaces/node'; -import {LQueries} from './interfaces/query'; -import {RComment, RElement} from './interfaces/renderer'; -import {StylingContext} from './interfaces/styling'; -import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, T_HOST} from './interfaces/view'; -import {getTNode, unwrapRNode} from './util/view_utils'; - -function attachDebugObject(obj: any, debug: any) { - Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); -} /* * This file contains conditionally attached classes which provide human readable (debug) level @@ -51,6 +54,80 @@ function attachDebugObject(obj: any, debug: any) { */ +export const LViewArray = ngDevMode && createNamedArrayType('LView'); +let LVIEW_EMPTY: unknown[]; // can't initialize here or it will not be tree shaken, because `LView` + // constructor could have side-effects. +/** + * This function clones a blueprint and creates LView. + * + * Simple slice will keep the same type, and we need it to be LView + */ +export function cloneToLView(list: any[]): LView { + if (LVIEW_EMPTY === undefined) LVIEW_EMPTY = new LViewArray !(); + return LVIEW_EMPTY.concat(list) as any; +} + +/** + * This class is a debug version of Object literal so that we can have constructor name show up in + * debug tools in ngDevMode. + */ +export const TViewConstructor = class TView implements ITView { + constructor( + public id: number, // + public blueprint: LView, // + public template: ComponentTemplate<{}>|null, // + public viewQuery: ViewQueriesFunction<{}>|null, // + public node: TViewNode|TElementNode|null, // + public data: TData, // + public bindingStartIndex: number, // + public viewQueryStartIndex: number, // + public expandoStartIndex: number, // + public expandoInstructions: ExpandoInstructions|null, // + public firstTemplatePass: boolean, // + public staticViewQueries: boolean, // + public staticContentQueries: boolean, // + public preOrderHooks: HookData|null, // + public preOrderCheckHooks: HookData|null, // + public contentHooks: HookData|null, // + public contentCheckHooks: HookData|null, // + public viewHooks: HookData|null, // + public viewCheckHooks: HookData|null, // + public destroyHooks: HookData|null, // + public cleanup: any[]|null, // + public contentQueries: number[]|null, // + public components: number[]|null, // + public directiveRegistry: DirectiveDefList|null, // + public pipeRegistry: PipeDefList|null, // + public firstChild: TNode|null, // + public schemas: SchemaMetadata[]|null, // + ) {} +}; + +const TViewData = ngDevMode && createNamedArrayType('TViewData'); +let TVIEWDATA_EMPTY: + unknown[]; // can't initialize here or it will not be tree shaken, because `LView` + // constructor could have side-effects. +/** + * This function clones a blueprint and creates TData. + * + * Simple slice will keep the same type, and we need it to be TData + */ +export function cloneToTViewData(list: any[]): TData { + if (TVIEWDATA_EMPTY === undefined) TVIEWDATA_EMPTY = new TViewData !(); + return TVIEWDATA_EMPTY.concat(list) as any; +} + +export const LViewBlueprint = ngDevMode && createNamedArrayType('LViewBlueprint'); +export const MatchesArray = ngDevMode && createNamedArrayType('MatchesArray'); +export const TViewComponents = ngDevMode && createNamedArrayType('TViewComponents'); +export const TNodeLocalNames = ngDevMode && createNamedArrayType('TNodeLocalNames'); +export const TNodeInitialInputs = ngDevMode && createNamedArrayType('TNodeInitialInputs'); +export const TNodeInitialData = ngDevMode && createNamedArrayType('TNodeInitialData'); +export const LCleanup = ngDevMode && createNamedArrayType('LCleanup'); +export const TCleanup = ngDevMode && createNamedArrayType('TCleanup'); + + + export function attachLViewDebug(lView: LView) { attachDebugObject(lView, new LViewDebug(lView)); } @@ -171,6 +248,8 @@ export class LViewDebug { export interface DebugNode { html: string|null; native: Node; + styles: DebugNewStyling|null; + classes: DebugNewStyling|null; nodes: DebugNode[]|null; component: LViewDebug|null; } @@ -188,12 +267,21 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul while (tNodeCursor) { const rawValue = lView[tNode.index]; const native = unwrapRNode(rawValue); - const componentLViewDebug = toDebug(readLViewValue(rawValue)); + const componentLViewDebug = + isStylingContext(rawValue) ? null : toDebug(readLViewValue(rawValue)); + + let styles: DebugNewStyling|null = null; + let classes: DebugNewStyling|null = null; + if (runtimeIsNewStylingInUse()) { + styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView, false) : null; + classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView, true) : null; + } + debugNodes.push({ html: toHtml(native), - native: native as any, + native: native as any, styles, classes, nodes: toDebugNodes(tNode.child, lView), - component: componentLViewDebug + component: componentLViewDebug, }); tNodeCursor = tNodeCursor.next; } diff --git a/packages/core/src/render3/instructions/projection.ts b/packages/core/src/render3/instructions/projection.ts index 301b43bc48..4a76d6e843 100644 --- a/packages/core/src/render3/instructions/projection.ts +++ b/packages/core/src/render3/instructions/projection.ts @@ -6,16 +6,47 @@ * found in the LICENSE file at https://angular.io/license */ import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node'; -import {CssSelectorList} from '../interfaces/projection'; -import {T_HOST} from '../interfaces/view'; +import {ProjectionSlots} from '../interfaces/projection'; +import {TVIEW, T_HOST} from '../interfaces/view'; import {appendProjectedNodes} from '../node_manipulation'; -import {matchingProjectionSelectorIndex} from '../node_selector_matcher'; -import {getLView, setIsParent} from '../state'; +import {getProjectAsAttrValue, isNodeMatchingSelectorList, isSelectorInSelectorList} from '../node_selector_matcher'; +import {getLView, setIsNotParent} from '../state'; import {findComponentView} from '../util/view_traversal_utils'; -import {createNodeAtIndex} from './shared'; +import {getOrCreateTNode} from './shared'; +/** + * Checks a given node against matching projection slots and returns the + * determined slot index. Returns "null" if no slot matched the given node. + * + * This function takes into account the parsed ngProjectAs selector from the + * node's attributes. If present, it will check whether the ngProjectAs selector + * matches any of the projection slot selectors. + */ +export function matchingProjectionSlotIndex(tNode: TNode, projectionSlots: ProjectionSlots): number| + null { + let wildcardNgContentIndex = null; + const ngProjectAsAttrVal = getProjectAsAttrValue(tNode); + for (let i = 0; i < projectionSlots.length; i++) { + const slotValue = projectionSlots[i]; + // The last wildcard projection slot should match all nodes which aren't matching + // any selector. This is necessary to be backwards compatible with view engine. + if (slotValue === '*') { + wildcardNgContentIndex = i; + continue; + } + // If we ran into an `ngProjectAs` attribute, we should match its parsed selector + // to the list of selectors, otherwise we fall back to matching against the node. + if (ngProjectAsAttrVal === null ? + isNodeMatchingSelectorList(tNode, slotValue, /* isProjectionMode */ true) : + isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)) { + return i; // first matching selector "captures" a given node + } + } + return wildcardNgContentIndex; +} + /** * Instruction to distribute projectable nodes among occurrences in a given template. * It takes all the selectors from the entire component's template and decides where @@ -34,32 +65,38 @@ import {createNodeAtIndex} from './shared'; * - we can't have only a parsed as we can't re-construct textual form from it (as entered by a * template author). * - * @param selectors A collection of parsed CSS selectors - * @param rawSelectors A collection of CSS selectors in the raw, un-parsed form + * @param projectionSlots? A collection of projection slots. A projection slot can be based + * on a parsed CSS selectors or set to the wildcard selector ("*") in order to match + * all nodes which do not match any selector. If not specified, a single wildcard + * selector projection slot will be defined. * * @codeGenApi */ -export function ɵɵprojectionDef(selectors?: CssSelectorList[]): void { +export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void { const componentNode = findComponentView(getLView())[T_HOST] as TElementNode; if (!componentNode.projection) { - const noOfNodeBuckets = selectors ? selectors.length + 1 : 1; + // If no explicit projection slots are defined, fall back to a single + // projection slot with the wildcard selector. + const numProjectionSlots = projectionSlots ? projectionSlots.length : 1; const projectionHeads: (TNode | null)[] = componentNode.projection = - new Array(noOfNodeBuckets).fill(null); + new Array(numProjectionSlots).fill(null); const tails: (TNode | null)[] = projectionHeads.slice(); let componentChild: TNode|null = componentNode.child; while (componentChild !== null) { - const bucketIndex = - selectors ? matchingProjectionSelectorIndex(componentChild, selectors) : 0; + const slotIndex = + projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0; - if (tails[bucketIndex]) { - tails[bucketIndex] !.projectionNext = componentChild; - } else { - projectionHeads[bucketIndex] = componentChild; + if (slotIndex !== null) { + if (tails[slotIndex]) { + tails[slotIndex] !.projectionNext = componentChild; + } else { + projectionHeads[slotIndex] = componentChild; + } + tails[slotIndex] = componentChild; } - tails[bucketIndex] = componentChild; componentChild = componentChild.next; } @@ -81,14 +118,14 @@ export function ɵɵprojectionDef(selectors?: CssSelectorList[]): void { export function ɵɵprojection( nodeIndex: number, selectorIndex: number = 0, attrs?: TAttributes): void { const lView = getLView(); - const tProjectionNode = - createNodeAtIndex(nodeIndex, TNodeType.Projection, null, null, attrs || null); + const tProjectionNode = getOrCreateTNode( + lView[TVIEW], lView[T_HOST], nodeIndex, TNodeType.Projection, null, attrs || null); // We can't use viewData[HOST_NODE] because projection nodes can be nested in embedded views. if (tProjectionNode.projection === null) tProjectionNode.projection = selectorIndex; // `` has no content - setIsParent(false); + setIsNotParent(); // re-distribution of projectable nodes is stored on a component's view level appendProjectedNodes(lView, tProjectionNode, selectorIndex, findComponentView(lView)); diff --git a/packages/core/src/render3/instructions/property.ts b/packages/core/src/render3/instructions/property.ts index 4a4b14ad91..8dd5d615e8 100644 --- a/packages/core/src/render3/instructions/property.ts +++ b/packages/core/src/render3/instructions/property.ts @@ -110,10 +110,12 @@ export function ɵɵelementProperty( * * @codeGenApi */ -export function ɵɵcomponentHostSyntheticProperty( - index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, - nativeOnly?: boolean) { - if (value !== NO_CHANGE) { - elementPropertyInternal(index, propName, value, sanitizer, nativeOnly, loadComponentRenderer); +export function ɵɵupdateSyntheticHostBinding( + propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean) { + const index = getSelectedIndex(); + // TODO(benlesh): remove bind call here. + const bound = ɵɵbind(value); + if (bound !== NO_CHANGE) { + elementPropertyInternal(index, propName, bound, sanitizer, nativeOnly, loadComponentRenderer); } } diff --git a/packages/core/src/render3/instructions/property_interpolation.ts b/packages/core/src/render3/instructions/property_interpolation.ts index 044d7d2b18..9d92cd853d 100644 --- a/packages/core/src/render3/instructions/property_interpolation.ts +++ b/packages/core/src/render3/instructions/property_interpolation.ts @@ -5,293 +5,14 @@ * 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 {assertEqual, assertLessThan} from '../../util/assert'; -import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from '../bindings'; import {SanitizerFn} from '../interfaces/sanitization'; -import {BINDING_INDEX, TVIEW} from '../interfaces/view'; -import {getLView, getSelectedIndex} from '../state'; +import {getSelectedIndex} from '../state'; import {NO_CHANGE} from '../tokens'; -import {renderStringify} from '../util/misc_utils'; -import {TsickleIssue1009, elementPropertyInternal, storeBindingMetadata} from './shared'; +import {ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolation4, ɵɵinterpolation5, ɵɵinterpolation6, ɵɵinterpolation7, ɵɵinterpolation8, ɵɵinterpolationV} from './interpolation'; +import {TsickleIssue1009, elementPropertyInternal} from './shared'; - -/** - * Create interpolation bindings with a variable number of expressions. - * - * If there are 1 to 8 expressions `interpolation1()` to `interpolation8()` should be used instead. - * Those are faster because there is no need to create an array of expressions and iterate over it. - * - * `values`: - * - has static text at even indexes, - * - has evaluated expressions at odd indexes. - * - * Returns the concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - * - * @codeGenApi - */ -export function ɵɵinterpolationV(values: any[]): string|NO_CHANGE { - ngDevMode && assertLessThan(2, values.length, 'should have at least 3 values'); - ngDevMode && assertEqual(values.length % 2, 1, 'should have an odd number of values'); - let different = false; - const lView = getLView(); - const tData = lView[TVIEW].data; - let bindingIndex = lView[BINDING_INDEX]; - - if (tData[bindingIndex] == null) { - // 2 is the index of the first static interstitial value (ie. not prefix) - for (let i = 2; i < values.length; i += 2) { - tData[bindingIndex++] = values[i]; - } - bindingIndex = lView[BINDING_INDEX]; - } - - for (let i = 1; i < values.length; i += 2) { - // Check if bindings (odd indexes) have changed - bindingUpdated(lView, bindingIndex++, values[i]) && (different = true); - } - lView[BINDING_INDEX] = bindingIndex; - storeBindingMetadata(lView, values[0], values[values.length - 1]); - - if (!different) { - return NO_CHANGE; - } - - // Build the updated content - let content = values[0]; - for (let i = 1; i < values.length; i += 2) { - content += renderStringify(values[i]) + values[i + 1]; - } - - return content; -} - -/** - * Creates an interpolation binding with 1 expression. - * - * @param prefix static value used for concatenation only. - * @param v0 value checked for change. - * @param suffix static value used for concatenation only. - * - * @codeGenApi - */ -export function ɵɵinterpolation1(prefix: string, v0: any, suffix: string): string|NO_CHANGE { - const lView = getLView(); - const different = bindingUpdated(lView, lView[BINDING_INDEX]++, v0); - storeBindingMetadata(lView, prefix, suffix); - return different ? prefix + renderStringify(v0) + suffix : NO_CHANGE; -} - -/** - * Creates an interpolation binding with 2 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation2( - prefix: string, v0: any, i0: string, v1: any, suffix: string): string|NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - const different = bindingUpdated2(lView, bindingIndex, v0, v1); - lView[BINDING_INDEX] += 2; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - lView[TVIEW].data[bindingIndex] = i0; - } - - return different ? prefix + renderStringify(v0) + i0 + renderStringify(v1) + suffix : NO_CHANGE; -} - -/** - * Creates an interpolation binding with 3 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation3( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string| - NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - const different = bindingUpdated3(lView, bindingIndex, v0, v1, v2); - lView[BINDING_INDEX] += 3; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + suffix : - NO_CHANGE; -} - -/** - * Create an interpolation binding with 4 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation4( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, - suffix: string): string|NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - const different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); - lView[BINDING_INDEX] += 4; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - tData[bindingIndex + 2] = i2; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + - renderStringify(v3) + suffix : - NO_CHANGE; -} - -/** - * Creates an interpolation binding with 5 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation5( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, - i3: string, v4: any, suffix: string): string|NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); - different = bindingUpdated(lView, bindingIndex + 4, v4) || different; - lView[BINDING_INDEX] += 5; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - tData[bindingIndex + 2] = i2; - tData[bindingIndex + 3] = i3; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + - renderStringify(v3) + i3 + renderStringify(v4) + suffix : - NO_CHANGE; -} - -/** - * Creates an interpolation binding with 6 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation6( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, - i3: string, v4: any, i4: string, v5: any, suffix: string): string|NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); - different = bindingUpdated2(lView, bindingIndex + 4, v4, v5) || different; - lView[BINDING_INDEX] += 6; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - tData[bindingIndex + 2] = i2; - tData[bindingIndex + 3] = i3; - tData[bindingIndex + 4] = i4; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + - renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + suffix : - NO_CHANGE; -} - -/** - * Creates an interpolation binding with 7 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation7( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, - i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string): string| - NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); - different = bindingUpdated3(lView, bindingIndex + 4, v4, v5, v6) || different; - lView[BINDING_INDEX] += 7; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - tData[bindingIndex + 2] = i2; - tData[bindingIndex + 3] = i3; - tData[bindingIndex + 4] = i4; - tData[bindingIndex + 5] = i5; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + - renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + - renderStringify(v6) + suffix : - NO_CHANGE; -} - -/** - * Creates an interpolation binding with 8 expressions. - * - * @codeGenApi - */ -export function ɵɵinterpolation8( - prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, - i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, - suffix: string): string|NO_CHANGE { - const lView = getLView(); - const bindingIndex = lView[BINDING_INDEX]; - let different = bindingUpdated4(lView, bindingIndex, v0, v1, v2, v3); - different = bindingUpdated4(lView, bindingIndex + 4, v4, v5, v6, v7) || different; - lView[BINDING_INDEX] += 8; - - // Only set static strings the first time (data will be null subsequent runs). - const data = storeBindingMetadata(lView, prefix, suffix); - if (data) { - const tData = lView[TVIEW].data; - tData[bindingIndex] = i0; - tData[bindingIndex + 1] = i1; - tData[bindingIndex + 2] = i2; - tData[bindingIndex + 3] = i3; - tData[bindingIndex + 4] = i4; - tData[bindingIndex + 5] = i5; - tData[bindingIndex + 6] = i6; - } - - return different ? - prefix + renderStringify(v0) + i0 + renderStringify(v1) + i1 + renderStringify(v2) + i2 + - renderStringify(v3) + i3 + renderStringify(v4) + i4 + renderStringify(v5) + i5 + - renderStringify(v6) + i6 + renderStringify(v7) + suffix : - NO_CHANGE; -} - -///////////////////////////////////////////////////////////////////// -/// NEW INSTRUCTIONS -///////////////////////////////////////////////////////////////////// - /** * * Update an interpolated property on an element with a lone bound value diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index ce2a8c1a29..6607f8d93b 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -12,10 +12,10 @@ import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, SchemaMetadata} from '../../me import {validateAgainstEventProperties} from '../../sanitization/sanitization'; import {Sanitizer} from '../../sanitization/security'; import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertLessThan, assertNotEqual, assertNotSame} from '../../util/assert'; +import {createNamedArrayType} from '../../util/named_array_type'; import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect'; import {assertLView, assertPreviousIsParent} from '../assert'; import {attachPatchData, getComponentViewByInstance} from '../context_discovery'; -import {attachLContainerDebug, attachLViewDebug} from '../debug'; import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di'; import {throwMultipleComponentError} from '../errors'; import {executeHooks, executePreOrderHooks, registerPreOrderHooks} from '../hooks'; @@ -30,7 +30,7 @@ import {StylingContext} from '../interfaces/styling'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, T_HOST} from '../interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from '../node_assert'; import {isNodeMatchingSelectorList} from '../node_selector_matcher'; -import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, isCreationMode, leaveView, resetComponentState, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex, ɵɵnamespaceHTML} from '../state'; +import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, incrementActiveDirectiveId, isCreationMode, leaveView, setActiveHostElement, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode, setSelectedIndex, ɵɵnamespaceHTML} from '../state'; import {initializeStaticContext as initializeStaticStylingContext} from '../styling/class_and_style_bindings'; import {ANIMATION_PROP_PREFIX, isAnimationProp} from '../styling/util'; import {NO_CHANGE} from '../tokens'; @@ -39,13 +39,15 @@ import {INTERPOLATION_DELIMITER, stringifyForError} from '../util/misc_utils'; import {getLViewParent, getRootContext} from '../util/view_traversal_utils'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isComponent, isComponentDef, isContentQueryHost, isLContainer, isRootView, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; +import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeInitialData, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLView, cloneToTViewData} from './lview_debug'; + /** * A permanent marker promise which signifies that the current CD tree is * clean. */ -const _CLEAN_PROMISE = Promise.resolve(null); +const _CLEAN_PROMISE = (() => Promise.resolve(null))(); export const enum BindingDirection { Input, @@ -199,13 +201,12 @@ export function elementCreate(name: string, overriddenRenderer?: Renderer3): REl } return native; } - export function createLView( parentLView: LView | null, tView: TView, context: T | null, flags: LViewFlags, host: RElement | null, tHostNode: TViewNode | TElementNode | null, rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null, sanitizer?: Sanitizer | null, injector?: Injector | null): LView { - const lView = tView.blueprint.slice() as LView; + const lView = ngDevMode ? cloneToLView(tView.blueprint) : tView.blueprint.slice() as LView; lView[HOST] = host; lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.FirstLViewPass; resetPreOrderHookFlags(lView); @@ -225,6 +226,10 @@ export function createLView( /** * Create and stores the TNode, and hooks it up to the tree. * + * @param tView The current `TView`. + * @param tHostNode This is a hack and we should not have to pass this value in. It is only used to + * determine if the parent belongs to a different tView. Instead we should not have parentTView + * point to TView other the current one. * @param index The index at which the TNode should be saved (null if view, since they are not * saved). * @param type The type of TNode to create @@ -232,50 +237,51 @@ export function createLView( * @param name The tag name of the associated native element, if applicable * @param attrs Any attrs for the native element, if applicable */ -export function createNodeAtIndex( - index: number, type: TNodeType.Element, native: RElement | RText | null, name: string | null, - attrs: TAttributes | null): TElementNode; -export function createNodeAtIndex( - index: number, type: TNodeType.Container, native: RComment, name: string | null, - attrs: TAttributes | null): TContainerNode; -export function createNodeAtIndex( - index: number, type: TNodeType.Projection, native: null, name: null, +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType.Element, + name: string | null, attrs: TAttributes | null): TElementNode; +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType.Container, + name: string | null, attrs: TAttributes | null): TContainerNode; +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType.Projection, name: null, attrs: TAttributes | null): TProjectionNode; -export function createNodeAtIndex( - index: number, type: TNodeType.ElementContainer, native: RComment, name: string | null, +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType.ElementContainer, + name: string | null, attrs: TAttributes | null): TElementContainerNode; +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType.IcuContainer, name: null, attrs: TAttributes | null): TElementContainerNode; -export function createNodeAtIndex( - index: number, type: TNodeType.IcuContainer, native: RComment, name: null, - attrs: TAttributes | null): TElementContainerNode; -export function createNodeAtIndex( - index: number, type: TNodeType, native: RText | RElement | RComment | null, name: string | null, +export function getOrCreateTNode( + tView: TView, tHostNode: TNode | null, index: number, type: TNodeType, name: string | null, attrs: TAttributes | null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode& TIcuContainerNode { - const lView = getLView(); - const tView = lView[TVIEW]; + // Keep this function short, so that the VM will inline it. const adjustedIndex = index + HEADER_OFFSET; - ngDevMode && - assertLessThan(adjustedIndex, lView.length, `Slot should have been initialized with null`); - lView[adjustedIndex] = native; + const tNode = tView.data[adjustedIndex] as TNode || + createTNodeAtIndex(tView, tHostNode, adjustedIndex, type, name, attrs, index); + setPreviousOrParentTNode(tNode, true); + return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode & + TProjectionNode & TIcuContainerNode; +} +function createTNodeAtIndex( + tView: TView, tHostNode: TNode | null, adjustedIndex: number, type: TNodeType, + name: string | null, attrs: TAttributes | null, index: number) { const previousOrParentTNode = getPreviousOrParentTNode(); const isParent = getIsParent(); - let tNode = tView.data[adjustedIndex] as TNode; - if (tNode == null) { - const parent = - isParent ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent; - - // Parents cannot cross component boundaries because components will be used in multiple places, - // so it's only set if the view is the same. - const parentInSameView = parent && parent !== lView[T_HOST]; - const tParentNode = parentInSameView ? parent as TElementNode | TContainerNode : null; - - tNode = tView.data[adjustedIndex] = createTNode(tParentNode, type, adjustedIndex, name, attrs); + const parent = + isParent ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent; + // Parents cannot cross component boundaries because components will be used in multiple places, + // so it's only set if the view is the same. + const parentInSameView = parent && parent !== tHostNode; + const tParentNode = parentInSameView ? parent as TElementNode | TContainerNode : null; + const tNode = tView.data[adjustedIndex] = + createTNode(tParentNode, type, adjustedIndex, name, attrs); + if (index === 0) { + tView.firstChild = tNode; } - // Now link ourselves into the tree. - // We need this even if tNode exists, otherwise we might end up pointing to unexisting tNodes when - // we use i18n (especially with ICU expressions that update the DOM during the update phase). if (previousOrParentTNode) { if (isParent && previousOrParentTNode.child == null && (tNode.parent !== null || previousOrParentTNode.type === TNodeType.View)) { @@ -285,15 +291,7 @@ export function createNodeAtIndex( previousOrParentTNode.next = tNode; } } - - if (tView.firstChild == null) { - tView.firstChild = tNode; - } - - setPreviousOrParentTNode(tNode); - setIsParent(true); - return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode & - TProjectionNode & TIcuContainerNode; + return tNode; } export function assignTViewNodeToLView( @@ -344,43 +342,6 @@ export function allocExpando(view: LView, numSlotsToAlloc: number) { //// Render ////////////////////////// -/** - * - * @param hostNode Existing node to render into. - * @param templateFn Template function with the instructions. - * @param consts The number of nodes, local refs, and pipes in this template - * @param context to pass into the template. - * @param providedRendererFactory renderer factory to use - * @param host The host element node to use - * @param directives Directive defs that should be used for matching - * @param pipes Pipe defs that should be used for matching - */ -export function renderTemplate( - hostNode: RElement, templateFn: ComponentTemplate, consts: number, vars: number, context: T, - providedRendererFactory: RendererFactory3, componentView: LView | null, - directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null, - sanitizer?: Sanitizer | null): LView { - if (componentView === null) { - resetComponentState(); - const renderer = providedRendererFactory.createRenderer(null, null); - - // We need to create a root view so it's possible to look up the host element through its index - const hostLView = createLView( - null, createTView(-1, null, 1, 0, null, null, null, null), {}, - LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null, providedRendererFactory, renderer); - enterView(hostLView, null); // SUSPECT! why do we need to enter the View? - - const componentTView = - getOrCreateTView(templateFn, consts, vars, directives || null, pipes || null, null, null); - const hostTNode = createNodeAtIndex(0, TNodeType.Element, hostNode, null, null); - componentView = createLView( - hostLView, componentTView, context, LViewFlags.CheckAlways, hostNode, hostTNode, - providedRendererFactory, renderer, sanitizer); - } - renderComponentOrTemplate(componentView, context, templateFn); - return componentView; -} - /** * Used for creating the LViewNode of a dynamic embedded view, * either through ViewContainerRef.createEmbeddedView() or TemplateRef.createEmbeddedView(). @@ -391,8 +352,7 @@ export function createEmbeddedViewAndNode( injectorIndex: number): LView { const _isParent = getIsParent(); const _previousOrParentTNode = getPreviousOrParentTNode(); - setIsParent(true); - setPreviousOrParentTNode(null !); + setPreviousOrParentTNode(null !, true); const lView = createLView(declarationView, tView, context, LViewFlags.CheckAlways, null, null); lView[DECLARATION_VIEW] = declarationView; @@ -406,8 +366,7 @@ export function createEmbeddedViewAndNode( tView.node !.injectorIndex = injectorIndex; } - setIsParent(_isParent); - setPreviousOrParentTNode(_previousOrParentTNode); + setPreviousOrParentTNode(_previousOrParentTNode, _isParent); return lView; } @@ -416,7 +375,8 @@ export function createEmbeddedViewAndNode( * * Dynamically created views must store/retrieve their TViews differently from component views * because their template functions are nested in the template functions of their hosts, creating - * closures. If their host template happens to be an embedded template in a loop (e.g. ngFor inside + * closures. If their host template happens to be an embedded template in a loop (e.g. ngFor + * inside * an ngFor), the nesting would mean we'd have multiple instances of the template function, so we * can't store TViews in the template function itself (as we do for comps). Instead, we store the * TView for dynamically created views on their host TNode, which only has one instance. @@ -430,8 +390,7 @@ export function renderEmbeddedTemplate(viewToRender: LView, tView: TView, con tickRootContext(getRootContext(viewToRender)); } else { try { - setIsParent(true); - setPreviousOrParentTNode(null !); + setPreviousOrParentTNode(null !, true); oldView = enterView(viewToRender, viewToRender[T_HOST]); resetPreOrderHookFlags(viewToRender); @@ -446,13 +405,12 @@ export function renderEmbeddedTemplate(viewToRender: LView, tView: TView, con refreshDescendantViews(viewToRender); } finally { leaveView(oldView !); - setIsParent(_isParent); - setPreviousOrParentTNode(_previousOrParentTNode); + setPreviousOrParentTNode(_previousOrParentTNode, _isParent); } } } -function renderComponentOrTemplate( +export function renderComponentOrTemplate( hostView: LView, context: T, templateFn?: ComponentTemplate) { const rendererFactory = hostView[RENDERER_FACTORY]; const oldView = enterView(hostView, hostView[T_HOST]); @@ -587,31 +545,16 @@ function saveResolvedLocalsInData( * Gets TView from a template function or creates a new TView * if it doesn't already exist. * - * @param templateFn The template from which to get static data - * @param consts The number of nodes, local refs, and pipes in this view - * @param vars The number of bindings and pure function bindings in this view - * @param directives Directive defs that should be saved on TView - * @param pipes Pipe defs that should be saved on TView - * @param viewQuery View query that should be saved on TView - * @param schemas Schemas that should be saved on TView + * @param def ComponentDef * @returns TView */ -export function getOrCreateTView( - templateFn: ComponentTemplate, consts: number, vars: number, - directives: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null, - viewQuery: ViewQueriesFunction| null, schemas: SchemaMetadata[] | null): TView { - // TODO(misko): reading `ngPrivateData` here is problematic for two reasons - // 1. It is a megamorphic call on each invocation. - // 2. For nested embedded views (ngFor inside ngFor) the template instance is per - // outer template invocation, which means that no such property will exist - // Correct solution is to only put `ngPrivateData` on the Component template - // and not on embedded templates. - - return templateFn.ngPrivateData || - (templateFn.ngPrivateData = createTView( - -1, templateFn, consts, vars, directives, pipes, viewQuery, schemas) as never); +export function getOrCreateTView(def: ComponentDef): TView { + return def.tView || (def.tView = createTView( + -1, def.template, def.consts, def.vars, def.directiveDefs, def.pipeDefs, + def.viewQuery, def.schemas)); } + /** * Creates a TView instance * @@ -634,39 +577,71 @@ export function createTView( // that has a host binding, we will update the blueprint with that def's hostVars count. const initialViewLength = bindingStartIndex + vars; const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength); - return blueprint[TVIEW as any] = { - id: viewIndex, - blueprint: blueprint, - template: templateFn, - viewQuery: viewQuery, - node: null !, - data: blueprint.slice().fill(null, bindingStartIndex), - bindingStartIndex: bindingStartIndex, - viewQueryStartIndex: initialViewLength, - expandoStartIndex: initialViewLength, - expandoInstructions: null, - firstTemplatePass: true, - staticViewQueries: false, - staticContentQueries: false, - preOrderHooks: null, - preOrderCheckHooks: null, - contentHooks: null, - contentCheckHooks: null, - viewHooks: null, - viewCheckHooks: null, - destroyHooks: null, - cleanup: null, - contentQueries: null, - components: null, - directiveRegistry: typeof directives === 'function' ? directives() : directives, - pipeRegistry: typeof pipes === 'function' ? pipes() : pipes, - firstChild: null, - schemas: schemas, - }; + return blueprint[TVIEW as any] = ngDevMode ? + new TViewConstructor( + viewIndex, // id: number, + blueprint, // blueprint: LView, + templateFn, // template: ComponentTemplate<{}>|null, + viewQuery, // viewQuery: ViewQueriesFunction<{}>|null, + null !, // node: TViewNode|TElementNode|null, + cloneToTViewData(blueprint).fill(null, bindingStartIndex), // data: TData, + bindingStartIndex, // bindingStartIndex: number, + initialViewLength, // viewQueryStartIndex: number, + initialViewLength, // expandoStartIndex: number, + null, // expandoInstructions: ExpandoInstructions|null, + true, // firstTemplatePass: boolean, + false, // staticViewQueries: boolean, + false, // staticContentQueries: boolean, + null, // preOrderHooks: HookData|null, + null, // preOrderCheckHooks: HookData|null, + null, // contentHooks: HookData|null, + null, // contentCheckHooks: HookData|null, + null, // viewHooks: HookData|null, + null, // viewCheckHooks: HookData|null, + null, // destroyHooks: HookData|null, + null, // cleanup: any[]|null, + null, // contentQueries: number[]|null, + null, // components: number[]|null, + typeof directives === 'function' ? + directives() : + directives, // directiveRegistry: DirectiveDefList|null, + typeof pipes === 'function' ? pipes() : pipes, // pipeRegistry: PipeDefList|null, + null, // firstChild: TNode|null, + schemas, // schemas: SchemaMetadata[]|null, + ) : + { + id: viewIndex, + blueprint: blueprint, + template: templateFn, + viewQuery: viewQuery, + node: null !, + data: blueprint.slice().fill(null, bindingStartIndex), + bindingStartIndex: bindingStartIndex, + viewQueryStartIndex: initialViewLength, + expandoStartIndex: initialViewLength, + expandoInstructions: null, + firstTemplatePass: true, + staticViewQueries: false, + staticContentQueries: false, + preOrderHooks: null, + preOrderCheckHooks: null, + contentHooks: null, + contentCheckHooks: null, + viewHooks: null, + viewCheckHooks: null, + destroyHooks: null, + cleanup: null, + contentQueries: null, + components: null, + directiveRegistry: typeof directives === 'function' ? directives() : directives, + pipeRegistry: typeof pipes === 'function' ? pipes() : pipes, + firstChild: null, + schemas: schemas, + }; } function createViewBlueprint(bindingStartIndex: number, initialViewLength: number): LView { - const blueprint = new Array(initialViewLength) + const blueprint = new (ngDevMode ? LViewBlueprint ! : Array)(initialViewLength) .fill(null, 0, bindingStartIndex) .fill(NO_CHANGE, bindingStartIndex) as LView; blueprint[BINDING_INDEX] = bindingStartIndex; @@ -779,6 +754,10 @@ export function createTNode( stylingTemplate: null, projection: null, onElementCreationFns: null, + // TODO (matsko): rename this to `styles` once the old styling impl is gone + newStyles: null, + // TODO (matsko): rename this to `classes` once the old styling impl is gone + newClasses: null, }; } @@ -820,8 +799,10 @@ export function generatePropertyAliases(tNode: TNode, direction: BindingDirectio } /** -* Mapping between attributes names that don't correspond to their element property names. -*/ + * Mapping between attributes names that don't correspond to their element property names. + * Note: this mapping has to be kept in sync with the equally named mapping in the template + * type-checking machinery of ngtsc. + */ const ATTR_TO_PROP: {[name: string]: string} = { 'class': 'className', 'for': 'htmlFor', @@ -872,7 +853,8 @@ export function elementPropertyInternal( savePropertyDebugData(tNode, lView, propName, lView[TVIEW].data, nativeOnly); const renderer = loadRendererFn ? loadRendererFn(tNode, lView) : lView[RENDERER]; - // It is assumed that the sanitizer is only added when the compiler determines that the property + // It is assumed that the sanitizer is only added when the compiler determines that the + // property // is risky, so sanitization can be done without further checks. value = sanitizer != null ? (sanitizer(value, tNode.tagName || '', propName) as any) : value; if (isProceduralRenderer(renderer)) { @@ -1018,7 +1000,8 @@ export function instantiateRootComponent( function resolveDirectives( tView: TView, viewData: LView, directives: DirectiveDef[] | null, tNode: TNode, localRefs: string[] | null): void { - // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in tsickle. + // Please make sure to have explicit type for `exportsMap`. Inferred type triggers bug in + // tsickle. ngDevMode && assertEqual(tView.firstTemplatePass, true, 'should run on first template pass only'); const exportsMap: ({[key: string]: number} | null) = localRefs ? {'': -1} : null; if (directives) { @@ -1184,7 +1167,6 @@ function postProcessBaseDirective( } - /** * Matches the current node against all available selectors. * If a component is matched (at most one), it is returned in first position in the array. @@ -1198,7 +1180,7 @@ function findDirectiveMatches(tView: TView, viewData: LView, tNode: TNode): Dire for (let i = 0; i < registry.length; i++) { const def = registry[i] as ComponentDef| DirectiveDef; if (isNodeMatchingSelectorList(tNode, def.selectors !, /* isProjectionMode */ false)) { - matches || (matches = []); + matches || (matches = ngDevMode ? new MatchesArray !() : []); diPublicInInjector( getOrCreateNodeInjectorForNode( getPreviousOrParentTNode() as TElementNode | TContainerNode | TElementContainerNode, @@ -1225,7 +1207,8 @@ export function queueComponentIndexForCheck(previousOrParentTNode: TNode): void const tView = getLView()[TVIEW]; ngDevMode && assertEqual(tView.firstTemplatePass, true, 'Should only be called in first template pass.'); - (tView.components || (tView.components = [])).push(previousOrParentTNode.index); + (tView.components || (tView.components = ngDevMode ? new TViewComponents !() : [ + ])).push(previousOrParentTNode.index); } @@ -1233,7 +1216,8 @@ export function queueComponentIndexForCheck(previousOrParentTNode: TNode): void function cacheMatchingLocalNames( tNode: TNode, localRefs: string[] | null, exportsMap: {[key: string]: number}): void { if (localRefs) { - const localNames: (string | number)[] = tNode.localNames = []; + const localNames: (string | number)[] = tNode.localNames = + ngDevMode ? new TNodeLocalNames !() : []; // Local names must be stored in tNode in the same order that localRefs are defined // in the template to ensure the data is loaded in the same slots as their refs @@ -1297,9 +1281,7 @@ function addComponentLogic( lView: LView, previousOrParentTNode: TNode, def: ComponentDef): void { const native = getNativeByTNode(previousOrParentTNode, lView); - const tView = getOrCreateTView( - def.template, def.consts, def.vars, def.directiveDefs, def.pipeDefs, def.viewQuery, - def.schemas); + const tView = getOrCreateTView(def); // Only component views should be added to the view tree directly. Embedded views are // accessed through their containers because they may be removed / re-added later. @@ -1308,7 +1290,7 @@ function addComponentLogic( lView, createLView( lView, tView, null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, lView[previousOrParentTNode.index], previousOrParentTNode as TElementNode, - rendererFactory, lView[RENDERER_FACTORY].createRenderer(native as RElement, def))); + rendererFactory, rendererFactory.createRenderer(native as RElement, def))); componentView[T_HOST] = previousOrParentTNode as TElementNode; @@ -1374,8 +1356,12 @@ function setInputsFromAttrs( */ function generateInitialInputs( directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData { - const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []); - initialInputData[directiveIndex] = null; + const initialInputData: InitialInputData = + tNode.initialInputs || (tNode.initialInputs = ngDevMode ? new TNodeInitialInputs !() : []); + // Ensure that we don't create sparse arrays + for (let i = initialInputData.length; i <= directiveIndex; i++) { + initialInputData.push(null); + } const attrs = tNode.attrs !; let i = 0; @@ -1398,8 +1384,8 @@ function generateInitialInputs( const attrValue = attrs[i + 1]; if (minifiedInputName !== undefined) { - const inputsToStore: InitialInputs = - initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); + const inputsToStore: InitialInputs = initialInputData[directiveIndex] || + (initialInputData[directiveIndex] = ngDevMode ? new TNodeInitialData !() : []); inputsToStore.push(attrName as string, minifiedInputName, attrValue as string); } @@ -1412,6 +1398,9 @@ function generateInitialInputs( //// ViewContainer & View ////////////////////////// +// Not sure why I need to do `any` here but TS complains later. +const LContainerArray: any = ngDevMode && createNamedArrayType('LContainer'); + /** * Creates a LContainer, either from a container instruction, or for a ViewContainerRef. * @@ -1427,23 +1416,25 @@ export function createLContainer( tNode: TNode, isForViewContainerRef?: boolean): LContainer { ngDevMode && assertDomNode(native); ngDevMode && assertLView(currentView); - const lContainer: LContainer = [ - hostNative, // host native - true, // Boolean `true` in this position signifies that this is an `LContainer` - isForViewContainerRef ? -1 : 0, // active index - currentView, // parent - null, // next - null, // queries - tNode, // t_host - native, // native - ]; + // https://jsperf.com/array-literal-vs-new-array-really + const lContainer: LContainer = new (ngDevMode ? LContainerArray : Array)( + hostNative, // host native + true, // Boolean `true` in this position signifies that this is an `LContainer` + isForViewContainerRef ? -1 : 0, // active index + currentView, // parent + null, // next + null, // queries + tNode, // t_host + native, // native + ); ngDevMode && attachLContainerDebug(lContainer); return lContainer; } /** - * Goes over dynamic embedded views (ones created through ViewContainerRef APIs) and refreshes them + * Goes over dynamic embedded views (ones created through ViewContainerRef APIs) and refreshes + * them * by executing an associated template function. */ function refreshDynamicEmbeddedViews(lView: LView) { @@ -1454,7 +1445,8 @@ function refreshDynamicEmbeddedViews(lView: LView) { if (current[ACTIVE_INDEX] === -1 && isLContainer(current)) { for (let i = CONTAINER_HEADER_OFFSET; i < current.length; i++) { const dynamicViewData = current[i]; - // The directives and pipes are not needed here as an existing view is only being refreshed. + // The directives and pipes are not needed here as an existing view is only being + // refreshed. ngDevMode && assertDefined(dynamicViewData[TVIEW], 'TView must be allocated'); renderEmbeddedTemplate(dynamicViewData, dynamicViewData[TVIEW], dynamicViewData[CONTEXT] !); } @@ -1531,9 +1523,12 @@ function syncViewWithBlueprint(componentView: LView) { * @returns The state passed in */ export function addToViewTree(lView: LView, lViewOrLContainer: T): T { - // TODO(benlesh/misko): This implementation is incorrect, because it always adds the LContainer to - // the end of the queue, which means if the developer retrieves the LContainers from RNodes out of - // order, the change detection will run out of order, as the act of retrieving the the LContainer + // TODO(benlesh/misko): This implementation is incorrect, because it always adds the LContainer + // to + // the end of the queue, which means if the developer retrieves the LContainers from RNodes out + // of + // order, the change detection will run out of order, as the act of retrieving the the + // LContainer // from the RNode is what adds it to the queue. if (lView[CHILD_HEAD]) { lView[CHILD_TAIL] ![NEXT] = lViewOrLContainer; @@ -1686,7 +1681,8 @@ export function checkNoChangesInRootView(lView: LView): void { } } -/** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */ +/** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. + */ export function checkView(hostView: LView, component: T) { const hostTView = hostView[TVIEW]; const oldView = enterView(hostView, hostView[T_HOST]); @@ -1746,27 +1742,23 @@ export function storeBindingMetadata(lView: LView, prefix = '', suffix = ''): st export const CLEAN_PROMISE = _CLEAN_PROMISE; -export function initializeTNodeInputs(tNode: TNode | null): PropertyAliases|null { +export function initializeTNodeInputs(tNode: TNode): PropertyAliases|null { // If tNode.inputs is undefined, a listener has created outputs, but inputs haven't // yet been checked. - if (tNode) { - if (tNode.inputs === undefined) { - // mark inputs as checked - tNode.inputs = generatePropertyAliases(tNode, BindingDirection.Input); - } - return tNode.inputs; + if (tNode.inputs === undefined) { + // mark inputs as checked + tNode.inputs = generatePropertyAliases(tNode, BindingDirection.Input); } - return null; + return tNode.inputs; } - export function getCleanup(view: LView): any[] { // top level variables should not be exported for performance reasons (PERF_NOTES.md) - return view[CLEANUP] || (view[CLEANUP] = []); + return view[CLEANUP] || (view[CLEANUP] = ngDevMode ? new LCleanup !() : []); } function getTViewCleanup(view: LView): any[] { - return view[TVIEW].cleanup || (view[TVIEW].cleanup = []); + return view[TVIEW].cleanup || (view[TVIEW].cleanup = ngDevMode ? new TCleanup !() : []); } /** diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 6735ea7b48..9fc8aec6b1 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -17,6 +17,9 @@ import {BoundPlayerFactory} from '../styling/player_factory'; import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared'; import {getCachedStylingContext, setCachedStylingContext} from '../styling/state'; import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util'; +import {classMap as newClassMap, classProp as newClassProp, styleMap as newStyleMap, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions'; +import {runtimeAllowOldStyling, runtimeIsNewStylingInUse} from '../styling_next/state'; +import {getBindingNameFromIndex} from '../styling_next/util'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getRootContext} from '../util/view_traversal_utils'; @@ -73,6 +76,13 @@ export function ɵɵstyling( const directiveStylingIndex = getActiveDirectiveStylingIndex(); if (directiveStylingIndex) { + // this is temporary hack to get the existing styling instructions to + // play ball with the new refactored implementation. + // TODO (matsko): remove this once the old implementation is not needed. + if (runtimeIsNewStylingInUse()) { + newStylingInit(); + } + // despite the binding being applied in a queue (below), the allocation // of the directive into the context happens right away. The reason for // this is to retain the ordering of the directives (which is important @@ -81,7 +91,7 @@ export function ɵɵstyling( const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || []; fns.push(() => { - initstyling( + initStyling( tNode, classBindingNames, styleBindingNames, styleSanitizer, directiveStylingIndex); registerHostDirective(tNode.stylingTemplate !, directiveStylingIndex); }); @@ -92,13 +102,13 @@ export function ɵɵstyling( // components) then they will be applied at the end of the `elementEnd` // instruction (because directives are created first before styling is // executed for a new element). - initstyling( + initStyling( tNode, classBindingNames, styleBindingNames, styleSanitizer, DEFAULT_TEMPLATE_DIRECTIVE_INDEX); } } -function initstyling( +function initStyling( tNode: TNode, classBindingNames: string[] | null | undefined, styleBindingNames: string[] | null | undefined, styleSanitizer: StyleSanitizeFn | null | undefined, directiveStylingIndex: number): void { @@ -148,6 +158,15 @@ export function ɵɵstyleProp( updatestyleProp( stylingContext, styleIndex, valueToAdd, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } + + if (runtimeIsNewStylingInUse()) { + const prop = getBindingNameFromIndex(stylingContext, styleIndex, directiveStylingIndex, false); + + // the reason why we cast the value as `boolean` is + // because the new styling refactor does not yet support + // sanitization or animation players. + newStyleProp(prop, value as string | number, suffix); + } } function resolveStylePropValue( @@ -206,6 +225,15 @@ export function ɵɵclassProp( updateclassProp( stylingContext, classIndex, input, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride); } + + if (runtimeIsNewStylingInUse()) { + const prop = getBindingNameFromIndex(stylingContext, classIndex, directiveStylingIndex, true); + + // the reason why we cast the value as `boolean` is + // because the new styling refactor does not yet support + // sanitization or animation players. + newClassProp(prop, input as boolean); + } } @@ -257,6 +285,10 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu } updateStyleMap(stylingContext, styles); } + + if (runtimeIsNewStylingInUse()) { + newStyleMap(styles); + } } @@ -300,6 +332,10 @@ export function ɵɵclassMap(classes: {[styleName: string]: any} | NO_CHANGE | s } updateClassMap(stylingContext, classes); } + + if (runtimeIsNewStylingInUse()) { + newClassMap(classes); + } } /** @@ -324,11 +360,14 @@ export function ɵɵstylingApply(): void { const renderer = tNode.type === TNodeType.Element ? lView[RENDERER] : null; const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; const stylingContext = getStylingContext(index, lView); - const totalPlayersQueued = renderStyling( - stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex); - if (totalPlayersQueued > 0) { - const rootContext = getRootContext(lView); - scheduleTick(rootContext, RootContextFlags.FlushPlayers); + + if (runtimeAllowOldStyling()) { + const totalPlayersQueued = renderStyling( + stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex); + if (totalPlayersQueued > 0) { + const rootContext = getRootContext(lView); + scheduleTick(rootContext, RootContextFlags.FlushPlayers); + } } // because select(n) may not run between every instruction, the cached styling @@ -339,6 +378,10 @@ export function ɵɵstylingApply(): void { // cleared because there is no code in Angular that applies more styling code after a // styling flush has occurred. Note that this will be fixed once FW-1254 lands. setCachedStylingContext(null); + + if (runtimeIsNewStylingInUse()) { + newStylingApply(); + } } export function getActiveDirectiveStylingIndex() { diff --git a/packages/core/src/render3/instructions/text.ts b/packages/core/src/render3/instructions/text.ts index 13bff5d10d..7d11848ec4 100644 --- a/packages/core/src/render3/instructions/text.ts +++ b/packages/core/src/render3/instructions/text.ts @@ -8,13 +8,13 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {TNodeType} from '../interfaces/node'; import {RText, isProceduralRenderer} from '../interfaces/renderer'; -import {BINDING_INDEX, HEADER_OFFSET, RENDERER, TVIEW} from '../interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, RENDERER, TVIEW, T_HOST} from '../interfaces/view'; import {appendChild, createTextNode} from '../node_manipulation'; -import {getLView, setIsParent} from '../state'; +import {getLView, setIsNotParent} from '../state'; import {NO_CHANGE} from '../tokens'; import {renderStringify} from '../util/misc_utils'; import {getNativeByIndex} from '../util/view_utils'; -import {createNodeAtIndex} from './shared'; +import {getOrCreateTNode} from './shared'; /** * Create static text node @@ -30,11 +30,12 @@ export function ɵɵtext(index: number, value?: any): void { lView[BINDING_INDEX], lView[TVIEW].bindingStartIndex, 'text nodes should be created before any bindings'); ngDevMode && ngDevMode.rendererCreateTextNode++; - const textNative = createTextNode(value, lView[RENDERER]); - const tNode = createNodeAtIndex(index, TNodeType.Element, textNative, null, null); + ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); + const textNative = lView[index + HEADER_OFFSET] = createTextNode(value, lView[RENDERER]); + const tNode = getOrCreateTNode(lView[TVIEW], lView[T_HOST], index, TNodeType.Element, null, null); // Text nodes are self closing. - setIsParent(false); + setIsNotParent(); appendChild(textNative, tNode, lView); } diff --git a/packages/core/src/render3/instructions/text_interpolation.ts b/packages/core/src/render3/instructions/text_interpolation.ts new file mode 100644 index 0000000000..827338249d --- /dev/null +++ b/packages/core/src/render3/instructions/text_interpolation.ts @@ -0,0 +1,298 @@ +/** + * @license + * Copyright Google Inc. 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 {getSelectedIndex} from '../state'; + +import {ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolation4, ɵɵinterpolation5, ɵɵinterpolation6, ɵɵinterpolation7, ɵɵinterpolation8, ɵɵinterpolationV} from './interpolation'; +import {TsickleIssue1009} from './shared'; +import {ɵɵtextBinding} from './text'; + + + +/** + * + * Update text content with a lone bound value + * + * Used when a text node has 1 interpolated value in it, an no additional text + * surrounds that interpolated value: + * + * ```html + *
{{v0}}
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate(v0); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate(v0: any): TsickleIssue1009 { + ɵɵtextInterpolate1('', v0, ''); + return ɵɵtextInterpolate; +} + + +/** + * + * Update text content with single bound value surrounded by other text. + * + * Used when a text node has 1 interpolated value in it: + * + * ```html + *
prefix{{v0}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate1('prefix', v0, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate1(prefix: string, v0: any, suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding(index, ɵɵinterpolation1(prefix, v0, suffix)); + return ɵɵtextInterpolate1; +} + +/** + * + * Update text content with 2 bound values surrounded by other text. + * + * Used when a text node has 2 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate2('prefix', v0, '-', v1, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate2( + prefix: string, v0: any, i0: string, v1: any, suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding(index, ɵɵinterpolation2(prefix, v0, i0, v1, suffix)); + return ɵɵtextInterpolate2; +} + +/** + * + * Update text content with 3 bound values surrounded by other text. + * + * Used when a text node has 3 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate3( + * 'prefix', v0, '-', v1, '-', v2, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate3( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, + suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding(index, ɵɵinterpolation3(prefix, v0, i0, v1, i1, v2, suffix)); + return ɵɵtextInterpolate3; +} + +/** + * + * Update text content with 4 bound values surrounded by other text. + * + * Used when a text node has 4 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate4( + * 'prefix', v0, '-', v1, '-', v2, '-', v3, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see ɵɵtextInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate4( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding(index, ɵɵinterpolation4(prefix, v0, i0, v1, i1, v2, i2, v3, suffix)); + return ɵɵtextInterpolate4; +} + +/** + * + * Update text content with 5 bound values surrounded by other text. + * + * Used when a text node has 5 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}-{{v4}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate5( + * 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate5( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding(index, ɵɵinterpolation5(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix)); + return ɵɵtextInterpolate5; +} + +/** + * + * Update text content with 6 bound values surrounded by other text. + * + * Used when a text node has 6 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}-{{v4}}-{{v5}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate6( + * 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, 'suffix'); + * ``` + * + * @param i4 Static value used for concatenation only. + * @param v5 Value checked for change. @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate6( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding( + index, ɵɵinterpolation6(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix)); + return ɵɵtextInterpolate6; +} + +/** + * + * Update text content with 7 bound values surrounded by other text. + * + * Used when a text node has 7 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}-{{v4}}-{{v5}}-{{v6}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate7( + * 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate7( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, + suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding( + index, ɵɵinterpolation7(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix)); + return ɵɵtextInterpolate7; +} + +/** + * + * Update text content with 8 bound values surrounded by other text. + * + * Used when a text node has 8 interpolated values in it: + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}-{{v4}}-{{v5}}-{{v6}}-{{v7}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolate8( + * 'prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, '-', v7, 'suffix'); + * ``` + * @returns itself, so that it may be chained. + * @see textInterpolateV + * @codeGenApi + */ +export function ɵɵtextInterpolate8( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, + suffix: string): TsickleIssue1009 { + const index = getSelectedIndex(); + ɵɵtextBinding( + index, + ɵɵinterpolation8(prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix)); + return ɵɵtextInterpolate8; +} + +/** + * Update text content with 9 or more bound values other surrounded by text. + * + * Used when the number of interpolated values exceeds 8. + * + * ```html + *
prefix{{v0}}-{{v1}}-{{v2}}-{{v3}}-{{v4}}-{{v5}}-{{v6}}-{{v7}}-{{v8}}-{{v9}}suffix
+ * ``` + * + * Its compiled representation is: + * + * ```ts + * ɵɵtextInterpolateV( + * ['prefix', v0, '-', v1, '-', v2, '-', v3, '-', v4, '-', v5, '-', v6, '-', v7, '-', v9, + * 'suffix']); + * ``` + *. + * @param values The a collection of values and the strings in between those values, beginning with + * a string prefix and ending with a string suffix. + * (e.g. `['prefix', value0, '-', value1, '-', value2, ..., value99, 'suffix']`) + * + * @returns itself, so that it may be chained. + * @codeGenApi + */ +export function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009 { + const index = getSelectedIndex(); + + ɵɵtextBinding(index, ɵɵinterpolationV(values)); + return ɵɵtextInterpolateV; +} diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 55effb84bb..5817ec267e 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -10,6 +10,7 @@ import {SchemaMetadata, ViewEncapsulation} from '../../core'; import {ProcessProvidersFunction} from '../../di/interface/provider'; import {Type} from '../../interface/type'; import {CssSelectorList} from './projection'; +import {TView} from './view'; /** @@ -19,7 +20,7 @@ export type ComponentTemplate = { // Note: the ctx parameter is typed as T|U, as using only U would prevent a template with // e.g. ctx: {} from being assigned to ComponentTemplate as TypeScript won't infer U = any // in that scenario. By including T this incompatibility is resolved. - (rf: RenderFlags, ctx: T | U): void; ngPrivateData?: never; + (rf: RenderFlags, ctx: T | U): void; }; /** @@ -302,6 +303,12 @@ export interface ComponentDef extends DirectiveDef { */ schemas: SchemaMetadata[]|null; + /** + * Ivy runtime uses this place to store the computed tView for the component. This gets filled on + * the first run of component. + */ + tView: TView|null; + /** * Used to store the result of `noSideEffects` function so that it is not removed by closure * compiler. The property should never be read. diff --git a/packages/core/src/render3/interfaces/injector.ts b/packages/core/src/render3/interfaces/injector.ts index e3c0e5541e..5fc27c72fe 100644 --- a/packages/core/src/render3/interfaces/injector.ts +++ b/packages/core/src/render3/interfaces/injector.ts @@ -241,10 +241,10 @@ export class NodeInjectorFactory { } } -const FactoryPrototype = NodeInjectorFactory.prototype; export function isFactory(obj: any): obj is NodeInjectorFactory { // See: https://jsperf.com/instanceof-vs-getprototypeof - return obj !== null && typeof obj == 'object' && Object.getPrototypeOf(obj) == FactoryPrototype; + return obj !== null && typeof obj == 'object' && + Object.getPrototypeOf(obj) == NodeInjectorFactory.prototype; } // Note: This hack is necessary so we don't erroneously get a circular dependency diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index c4f476ce7c..4e88949712 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -5,7 +5,7 @@ * 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 {TStylingContext} from '../styling_next/interfaces'; import {CssSelector} from './projection'; import {RNode} from './renderer'; import {StylingContext} from './styling'; @@ -180,7 +180,23 @@ export const enum AttributeMarker { * ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']] * ``` */ - ProjectAs = 5 + ProjectAs = 5, + + /** + * Signals that the following attribute will be translated by runtime i18n + * + * For example, given the following HTML: + * + * ``` + *
+ * ``` + * + * the generated code is: + * + * ``` + * var _c1 = ['moo', 'car', AttributeMarker.I18n, 'foo', 'bar']; + */ + I18n, } /** @@ -438,6 +454,10 @@ export interface TNode { * with functions each time the creation block is called. */ onElementCreationFns: Function[]|null; + // TODO (matsko): rename this to `styles` once the old styling impl is gone + newStyles: TStylingContext|null; + // TODO (matsko): rename this to `classes` once the old styling impl is gone + newClasses: TStylingContext|null; } /** Static data for an element */ diff --git a/packages/core/src/render3/interfaces/projection.ts b/packages/core/src/render3/interfaces/projection.ts index 891dd28135..6657ab7ff9 100644 --- a/packages/core/src/render3/interfaces/projection.ts +++ b/packages/core/src/render3/interfaces/projection.ts @@ -50,6 +50,16 @@ export type CssSelector = (string | SelectorFlags)[]; */ export type CssSelectorList = CssSelector[]; +/** + * List of slots for a projection. A slot can be either based on a parsed CSS selector + * which will be used to determine nodes which are projected into that slot. + * + * When set to "*", the slot is reserved and can be used for multi-slot projection + * using {@link ViewContainerRef#createComponent}. The last slot that specifies the + * wildcard selector will retrieve all projectable nodes which do not match any selector. + */ +export type ProjectionSlots = (CssSelectorList | '*')[]; + /** Flags used to build up CssSelectors */ export const enum SelectorFlags { /** Indicates this is the beginning of a new negative selector */ diff --git a/packages/core/src/render3/interfaces/query.ts b/packages/core/src/render3/interfaces/query.ts index 9aad007f98..0615ec7946 100644 --- a/packages/core/src/render3/interfaces/query.ts +++ b/packages/core/src/render3/interfaces/query.ts @@ -25,11 +25,32 @@ export interface LQueries { parent: LQueries|null; /** - * Ask queries to prepare copy of itself. This assures that tracking new queries on content nodes - * doesn't mutate list of queries tracked on a parent node. We will clone LQueries before - * constructing content queries. + * The index of the node on which this LQueries instance was created / cloned in a given LView. + * + * This index is stored to minimize LQueries cloning: we can observe that LQueries can be mutated + * only under 2 conditions: + * - we are crossing an element that has directives with content queries (new queries are added); + * - we are descending into element hierarchy (creating a child element of an existing element) + * and the current LQueries object is tracking shallow queries (shallow queries are removed). + * + * Since LQueries are not cloned systematically we need to know exactly where (on each element) + * cloning occurred, so we can properly restore the set of tracked queries when going up the + * elements hierarchy. + * + * Always set to -1 for view queries as view queries are created before we process any node in a + * given view. */ - clone(): LQueries; + nodeIndex: number; + + /** + * Ask queries to prepare a copy of itself. This ensures that: + * - tracking new queries on content nodes doesn't mutate list of queries tracked on a parent + * node; + * - we don't track shallow queries when descending into elements hierarchy. + * + * We will clone LQueries before constructing content queries + */ + clone(tNode: TNode): LQueries; /** * Notify `LQueries` that a new `TNode` has been created and needs to be added to query results diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 9383153210..acf341ca0b 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -327,7 +327,7 @@ export interface ExpandoInstructions extends Array ({ + 'ɵɵattribute': r3.ɵɵattribute, + 'ɵɵattributeInterpolate1': r3.ɵɵattributeInterpolate1, + 'ɵɵattributeInterpolate2': r3.ɵɵattributeInterpolate2, + 'ɵɵattributeInterpolate3': r3.ɵɵattributeInterpolate3, + 'ɵɵattributeInterpolate4': r3.ɵɵattributeInterpolate4, + 'ɵɵattributeInterpolate5': r3.ɵɵattributeInterpolate5, + 'ɵɵattributeInterpolate6': r3.ɵɵattributeInterpolate6, + 'ɵɵattributeInterpolate7': r3.ɵɵattributeInterpolate7, + 'ɵɵattributeInterpolate8': r3.ɵɵattributeInterpolate8, + 'ɵɵattributeInterpolateV': r3.ɵɵattributeInterpolateV, + 'ɵɵdefineBase': r3.ɵɵdefineBase, + 'ɵɵdefineComponent': r3.ɵɵdefineComponent, + 'ɵɵdefineDirective': r3.ɵɵdefineDirective, + 'ɵɵdefineInjectable': ɵɵdefineInjectable, + 'ɵɵdefineInjector': ɵɵdefineInjector, + 'ɵɵdefineNgModule': r3.ɵɵdefineNgModule, + 'ɵɵdefinePipe': r3.ɵɵdefinePipe, + 'ɵɵdirectiveInject': r3.ɵɵdirectiveInject, + 'ɵɵgetFactoryOf': r3.ɵɵgetFactoryOf, + 'ɵɵgetInheritedFactory': r3.ɵɵgetInheritedFactory, + 'ɵɵinject': ɵɵinject, + 'ɵɵinjectAttribute': r3.ɵɵinjectAttribute, + 'ɵɵtemplateRefExtractor': r3.ɵɵtemplateRefExtractor, + 'ɵɵNgOnChangesFeature': r3.ɵɵNgOnChangesFeature, + 'ɵɵProvidersFeature': r3.ɵɵProvidersFeature, + 'ɵɵInheritDefinitionFeature': r3.ɵɵInheritDefinitionFeature, + 'ɵɵelementAttribute': r3.ɵɵelementAttribute, + 'ɵɵbind': r3.ɵɵbind, + 'ɵɵcontainer': r3.ɵɵcontainer, + 'ɵɵnextContext': r3.ɵɵnextContext, + 'ɵɵcontainerRefreshStart': r3.ɵɵcontainerRefreshStart, + 'ɵɵcontainerRefreshEnd': r3.ɵɵcontainerRefreshEnd, + 'ɵɵnamespaceHTML': r3.ɵɵnamespaceHTML, + 'ɵɵnamespaceMathML': r3.ɵɵnamespaceMathML, + 'ɵɵnamespaceSVG': r3.ɵɵnamespaceSVG, + 'ɵɵenableBindings': r3.ɵɵenableBindings, + 'ɵɵdisableBindings': r3.ɵɵdisableBindings, + 'ɵɵallocHostVars': r3.ɵɵallocHostVars, + 'ɵɵelementStart': r3.ɵɵelementStart, + 'ɵɵelementEnd': r3.ɵɵelementEnd, + 'ɵɵelement': r3.ɵɵelement, + 'ɵɵelementContainerStart': r3.ɵɵelementContainerStart, + 'ɵɵelementContainerEnd': r3.ɵɵelementContainerEnd, + 'ɵɵpureFunction0': r3.ɵɵpureFunction0, + 'ɵɵpureFunction1': r3.ɵɵpureFunction1, + 'ɵɵpureFunction2': r3.ɵɵpureFunction2, + 'ɵɵpureFunction3': r3.ɵɵpureFunction3, + 'ɵɵpureFunction4': r3.ɵɵpureFunction4, + 'ɵɵpureFunction5': r3.ɵɵpureFunction5, + 'ɵɵpureFunction6': r3.ɵɵpureFunction6, + 'ɵɵpureFunction7': r3.ɵɵpureFunction7, + 'ɵɵpureFunction8': r3.ɵɵpureFunction8, + 'ɵɵpureFunctionV': r3.ɵɵpureFunctionV, + 'ɵɵgetCurrentView': r3.ɵɵgetCurrentView, + 'ɵɵrestoreView': r3.ɵɵrestoreView, + 'ɵɵinterpolation1': r3.ɵɵinterpolation1, + 'ɵɵinterpolation2': r3.ɵɵinterpolation2, + 'ɵɵinterpolation3': r3.ɵɵinterpolation3, + 'ɵɵinterpolation4': r3.ɵɵinterpolation4, + 'ɵɵinterpolation5': r3.ɵɵinterpolation5, + 'ɵɵinterpolation6': r3.ɵɵinterpolation6, + 'ɵɵinterpolation7': r3.ɵɵinterpolation7, + 'ɵɵinterpolation8': r3.ɵɵinterpolation8, + 'ɵɵinterpolationV': r3.ɵɵinterpolationV, + 'ɵɵlistener': r3.ɵɵlistener, + 'ɵɵload': r3.ɵɵload, + 'ɵɵprojection': r3.ɵɵprojection, + 'ɵɵelementProperty': r3.ɵɵelementProperty, + 'ɵɵupdateSyntheticHostBinding': r3.ɵɵupdateSyntheticHostBinding, + 'ɵɵcomponentHostSyntheticListener': r3.ɵɵcomponentHostSyntheticListener, + 'ɵɵpipeBind1': r3.ɵɵpipeBind1, + 'ɵɵpipeBind2': r3.ɵɵpipeBind2, + 'ɵɵpipeBind3': r3.ɵɵpipeBind3, + 'ɵɵpipeBind4': r3.ɵɵpipeBind4, + 'ɵɵpipeBindV': r3.ɵɵpipeBindV, + 'ɵɵprojectionDef': r3.ɵɵprojectionDef, + 'ɵɵproperty': r3.ɵɵproperty, + 'ɵɵpropertyInterpolate': r3.ɵɵpropertyInterpolate, + 'ɵɵpropertyInterpolate1': r3.ɵɵpropertyInterpolate1, + 'ɵɵpropertyInterpolate2': r3.ɵɵpropertyInterpolate2, + 'ɵɵpropertyInterpolate3': r3.ɵɵpropertyInterpolate3, + 'ɵɵpropertyInterpolate4': r3.ɵɵpropertyInterpolate4, + 'ɵɵpropertyInterpolate5': r3.ɵɵpropertyInterpolate5, + 'ɵɵpropertyInterpolate6': r3.ɵɵpropertyInterpolate6, + 'ɵɵpropertyInterpolate7': r3.ɵɵpropertyInterpolate7, + 'ɵɵpropertyInterpolate8': r3.ɵɵpropertyInterpolate8, + 'ɵɵpropertyInterpolateV': r3.ɵɵpropertyInterpolateV, + 'ɵɵpipe': r3.ɵɵpipe, + 'ɵɵqueryRefresh': r3.ɵɵqueryRefresh, + 'ɵɵviewQuery': r3.ɵɵviewQuery, + 'ɵɵstaticViewQuery': r3.ɵɵstaticViewQuery, + 'ɵɵstaticContentQuery': r3.ɵɵstaticContentQuery, + 'ɵɵloadViewQuery': r3.ɵɵloadViewQuery, + 'ɵɵcontentQuery': r3.ɵɵcontentQuery, + 'ɵɵloadContentQuery': r3.ɵɵloadContentQuery, + 'ɵɵreference': r3.ɵɵreference, + 'ɵɵelementHostAttrs': r3.ɵɵelementHostAttrs, + 'ɵɵclassMap': r3.ɵɵclassMap, + 'ɵɵstyling': r3.ɵɵstyling, + 'ɵɵstyleMap': r3.ɵɵstyleMap, + 'ɵɵstyleProp': r3.ɵɵstyleProp, + 'ɵɵstyleSanitizer': r3.ɵɵstyleSanitizer, + 'ɵɵstylingApply': r3.ɵɵstylingApply, + 'ɵɵclassProp': r3.ɵɵclassProp, + 'ɵɵselect': r3.ɵɵselect, + 'ɵɵtemplate': r3.ɵɵtemplate, + 'ɵɵtext': r3.ɵɵtext, + 'ɵɵtextBinding': r3.ɵɵtextBinding, + 'ɵɵtextInterpolate': r3.ɵɵtextInterpolate, + 'ɵɵtextInterpolate1': r3.ɵɵtextInterpolate1, + 'ɵɵtextInterpolate2': r3.ɵɵtextInterpolate2, + 'ɵɵtextInterpolate3': r3.ɵɵtextInterpolate3, + 'ɵɵtextInterpolate4': r3.ɵɵtextInterpolate4, + 'ɵɵtextInterpolate5': r3.ɵɵtextInterpolate5, + 'ɵɵtextInterpolate6': r3.ɵɵtextInterpolate6, + 'ɵɵtextInterpolate7': r3.ɵɵtextInterpolate7, + 'ɵɵtextInterpolate8': r3.ɵɵtextInterpolate8, + 'ɵɵtextInterpolateV': r3.ɵɵtextInterpolateV, + 'ɵɵembeddedViewStart': r3.ɵɵembeddedViewStart, + 'ɵɵembeddedViewEnd': r3.ɵɵembeddedViewEnd, + 'ɵɵi18n': r3.ɵɵi18n, + 'ɵɵi18nAttributes': r3.ɵɵi18nAttributes, + 'ɵɵi18nExp': r3.ɵɵi18nExp, + 'ɵɵi18nStart': r3.ɵɵi18nStart, + 'ɵɵi18nEnd': r3.ɵɵi18nEnd, + 'ɵɵi18nApply': r3.ɵɵi18nApply, + 'ɵɵi18nPostprocess': r3.ɵɵi18nPostprocess, + 'ɵɵi18nLocalize': r3.ɵɵi18nLocalize, + 'ɵɵresolveWindow': r3.ɵɵresolveWindow, + 'ɵɵresolveDocument': r3.ɵɵresolveDocument, + 'ɵɵresolveBody': r3.ɵɵresolveBody, + 'ɵɵsetComponentScope': r3.ɵɵsetComponentScope, + 'ɵɵsetNgModuleScope': r3.ɵɵsetNgModuleScope, - 'ɵɵsanitizeHtml': sanitization.ɵɵsanitizeHtml, - 'ɵɵsanitizeStyle': sanitization.ɵɵsanitizeStyle, - 'ɵɵdefaultStyleSanitizer': sanitization.ɵɵdefaultStyleSanitizer, - 'ɵɵsanitizeResourceUrl': sanitization.ɵɵsanitizeResourceUrl, - 'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript, - 'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl, - 'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl, - - 'ɵregisterNgModuleType': registerNgModuleType, -}; + 'ɵɵsanitizeHtml': sanitization.ɵɵsanitizeHtml, + 'ɵɵsanitizeStyle': sanitization.ɵɵsanitizeStyle, + 'ɵɵdefaultStyleSanitizer': sanitization.ɵɵdefaultStyleSanitizer, + 'ɵɵsanitizeResourceUrl': sanitization.ɵɵsanitizeResourceUrl, + 'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript, + 'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl, + 'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl, + }))(); diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index 3777b0cf1c..a0f6145254 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -11,7 +11,6 @@ import {resolveForwardRef} from '../../di/forward_ref'; import {NG_INJECTOR_DEF} from '../../di/interface/defs'; import {reflectDependencies} from '../../di/jit/util'; import {Type} from '../../interface/type'; -import {registerNgModuleType} from '../../linker/ng_module_factory_loader'; import {Component} from '../../metadata'; import {ModuleWithProviders, NgModule, NgModuleDef, NgModuleTransitiveScopes} from '../../metadata/ng_module'; import {flatten} from '../../util/array_utils'; @@ -95,12 +94,16 @@ export function compileNgModule(moduleType: Type, ngModule: NgModule = {}): /** * Compiles and adds the `ngModuleDef` and `ngInjectorDef` properties to the module class. + * + * It's possible to compile a module via this API which will allow duplicate declarations in its + * root. */ -export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule): void { +export function compileNgModuleDefs( + moduleType: NgModuleType, ngModule: NgModule, + allowDuplicateDeclarationsInRoot: boolean = false): void { ngDevMode && assertDefined(moduleType, 'Required value moduleType'); ngDevMode && assertDefined(ngModule, 'Required value ngModule'); const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); - let ngModuleDef: any = null; Object.defineProperty(moduleType, NG_MODULE_DEF, { configurable: true, @@ -109,28 +112,29 @@ export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule ngModuleDef = getCompilerFacade().compileNgModule( angularCoreEnv, `ng:///${moduleType.name}/ngModuleDef.js`, { type: moduleType, - bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY, resolveForwardRef), + bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY).map(resolveForwardRef), declarations: declarations.map(resolveForwardRef), - imports: flatten(ngModule.imports || EMPTY_ARRAY, resolveForwardRef) + imports: flatten(ngModule.imports || EMPTY_ARRAY) + .map(resolveForwardRef) .map(expandModuleWithProviders), - exports: flatten(ngModule.exports || EMPTY_ARRAY, resolveForwardRef) + exports: flatten(ngModule.exports || EMPTY_ARRAY) + .map(resolveForwardRef) .map(expandModuleWithProviders), emitInline: true, schemas: ngModule.schemas ? flatten(ngModule.schemas) : null, + id: ngModule.id || null, }); } return ngModuleDef; } }); - if (ngModule.id) { - registerNgModuleType(ngModule.id, moduleType); - } let ngInjectorDef: any = null; Object.defineProperty(moduleType, NG_INJECTOR_DEF, { get: () => { if (ngInjectorDef === null) { - ngDevMode && verifySemanticsOfNgModuleDef(moduleType as any as NgModuleType); + ngDevMode && verifySemanticsOfNgModuleDef( + moduleType as any as NgModuleType, allowDuplicateDeclarationsInRoot); const meta: R3InjectorMetadataFacade = { name: moduleType.name, type: moduleType, @@ -151,7 +155,8 @@ export function compileNgModuleDefs(moduleType: NgModuleType, ngModule: NgModule }); } -function verifySemanticsOfNgModuleDef(moduleType: NgModuleType): void { +function verifySemanticsOfNgModuleDef( + moduleType: NgModuleType, allowDuplicateDeclarationsInRoot: boolean): void { if (verifiedNgModule.get(moduleType)) return; verifiedNgModule.set(moduleType, true); moduleType = resolveForwardRef(moduleType); @@ -159,22 +164,25 @@ function verifySemanticsOfNgModuleDef(moduleType: NgModuleType): void { const errors: string[] = []; const declarations = maybeUnwrapFn(ngModuleDef.declarations); const imports = maybeUnwrapFn(ngModuleDef.imports); - flatten(imports, unwrapModuleWithProvidersImports).forEach(verifySemanticsOfNgModuleDef); + flatten(imports) + .map(unwrapModuleWithProvidersImports) + .forEach(mod => verifySemanticsOfNgModuleDef(mod, false)); const exports = maybeUnwrapFn(ngModuleDef.exports); declarations.forEach(verifyDeclarationsHaveDefinitions); const combinedDeclarations: Type[] = [ ...declarations.map(resolveForwardRef), // - ...flatten(imports.map(computeCombinedExports), resolveForwardRef), + ...flatten(imports.map(computeCombinedExports)).map(resolveForwardRef), ]; exports.forEach(verifyExportsAreDeclaredOrReExported); - declarations.forEach(verifyDeclarationIsUnique); + declarations.forEach(decl => verifyDeclarationIsUnique(decl, allowDuplicateDeclarationsInRoot)); declarations.forEach(verifyComponentEntryComponentsIsPartOfNgModule); const ngModule = getAnnotation(moduleType, 'NgModule'); if (ngModule) { ngModule.imports && - flatten(ngModule.imports, unwrapModuleWithProvidersImports) - .forEach(verifySemanticsOfNgModuleDef); + flatten(ngModule.imports) + .map(unwrapModuleWithProvidersImports) + .forEach(mod => verifySemanticsOfNgModuleDef(mod, false)); ngModule.bootstrap && ngModule.bootstrap.forEach(verifyCorrectBootstrapType); ngModule.bootstrap && ngModule.bootstrap.forEach(verifyComponentIsPartOfNgModule); ngModule.entryComponents && ngModule.entryComponents.forEach(verifyComponentIsPartOfNgModule); @@ -209,15 +217,17 @@ function verifySemanticsOfNgModuleDef(moduleType: NgModuleType): void { } } - function verifyDeclarationIsUnique(type: Type) { + function verifyDeclarationIsUnique(type: Type, suppressErrors: boolean) { type = resolveForwardRef(type); const existingModule = ownerNgModule.get(type); if (existingModule && existingModule !== moduleType) { - const modules = [existingModule, moduleType].map(stringifyForError).sort(); - errors.push( - `Type ${stringifyForError(type)} is part of the declarations of 2 modules: ${modules[0]} and ${modules[1]}! ` + - `Please consider moving ${stringifyForError(type)} to a higher module that imports ${modules[0]} and ${modules[1]}. ` + - `You can also create a new NgModule that exports and includes ${stringifyForError(type)} then import that NgModule in ${modules[0]} and ${modules[1]}.`); + if (!suppressErrors) { + const modules = [existingModule, moduleType].map(stringifyForError).sort(); + errors.push( + `Type ${stringifyForError(type)} is part of the declarations of 2 modules: ${modules[0]} and ${modules[1]}! ` + + `Please consider moving ${stringifyForError(type)} to a higher module that imports ${modules[0]} and ${modules[1]}. ` + + `You can also create a new NgModule that exports and includes ${stringifyForError(type)} then import that NgModule in ${modules[0]} and ${modules[1]}.`); + } } else { // Mark type as having owner. ownerNgModule.set(type, moduleType); @@ -312,7 +322,7 @@ function computeCombinedExports(type: Type): Type[] { return [...flatten(maybeUnwrapFn(ngModuleDef.exports).map((type) => { const ngModuleDef = getNgModuleDef(type); if (ngModuleDef) { - verifySemanticsOfNgModuleDef(type as any as NgModuleType); + verifySemanticsOfNgModuleDef(type as any as NgModuleType, false); return computeCombinedExports(type); } else { return type; @@ -364,7 +374,7 @@ export function patchComponentDefWithScope( // may face a problem where previously compiled defs available to a given Component/Directive // are cached in TView and may become stale (in case any of these defs gets recompiled). In // order to avoid this problem, we force fresh TView to be created. - componentDef.template.ngPrivateData = undefined; + componentDef.tView = null; } /** diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts index 889548a86b..f46b63e0b6 100644 --- a/packages/core/src/render3/ng_module_ref.ts +++ b/packages/core/src/render3/ng_module_ref.ts @@ -14,11 +14,14 @@ import {R3Injector, createInjector} from '../di/r3_injector'; import {Type} from '../interface/type'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory'; +import {registerNgModuleType} from '../linker/ng_module_factory_registration'; import {NgModuleDef} from '../metadata/ng_module'; import {assertDefined} from '../util/assert'; import {stringify} from '../util/stringify'; + import {ComponentFactoryResolver} from './component_ref'; -import {getNgModuleDef} from './definition'; +import {getNgLocaleIdDef, getNgModuleDef} from './definition'; +import {setLocaleId} from './i18n'; import {maybeUnwrapFn} from './util/misc_utils'; export interface NgModuleType extends Type { ngModuleDef: NgModuleDef; } @@ -45,6 +48,11 @@ export class NgModuleRef extends viewEngine_NgModuleRef implements Interna ngModuleDef, `NgModule '${stringify(ngModuleType)}' is not a subtype of 'NgModuleType'.`); + const ngLocaleIdDef = getNgLocaleIdDef(ngModuleType); + if (ngLocaleIdDef) { + setLocaleId(ngLocaleIdDef); + } + this._bootstrapComponents = maybeUnwrapFn(ngModuleDef !.bootstrap); const additionalProviders: StaticProvider[] = [ { @@ -84,7 +92,37 @@ export class NgModuleRef extends viewEngine_NgModuleRef implements Interna } export class NgModuleFactory extends viewEngine_NgModuleFactory { - constructor(public moduleType: Type) { super(); } + constructor(public moduleType: Type) { + super(); + + const ngModuleDef = getNgModuleDef(moduleType); + if (ngModuleDef !== null) { + // Register the NgModule with Angular's module registry. The location (and hence timing) of + // this call is critical to ensure this works correctly (modules get registered when expected) + // without bloating bundles (modules are registered when otherwise not referenced). + // + // In View Engine, registration occurs in the .ngfactory.js file as a side effect. This has + // several practical consequences: + // + // - If an .ngfactory file is not imported from, the module won't be registered (and can be + // tree shaken). + // - If an .ngfactory file is imported from, the module will be registered even if an instance + // is not actually created (via `create` below). + // - Since an .ngfactory file in View Engine references the .ngfactory files of the NgModule's + // imports, + // + // In Ivy, things are a bit different. .ngfactory files still exist for compatibility, but are + // not a required API to use - there are other ways to obtain an NgModuleFactory for a given + // NgModule. Thus, relying on a side effect in the .ngfactory file is not sufficient. Instead, + // the side effect of registration is added here, in the constructor of NgModuleFactory, + // ensuring no matter how a factory is created, the module is registered correctly. + // + // An alternative would be to include the registration side effect inline following the actual + // NgModule definition. This also has the correct timing, but breaks tree-shaking - modules + // will be registered and retained even if they're otherwise never referenced. + registerNgModuleType(moduleType as NgModuleType); + } + } create(parentInjector: Injector|null): viewEngine_NgModuleRef { return new NgModuleRef(this.moduleType, parentInjector); diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 2392393567..8fc447f32b 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -15,7 +15,7 @@ import {ComponentDef} from './interfaces/definition'; import {NodeInjectorFactory} from './interfaces/injector'; import {TElementNode, TNode, TNodeFlags, TNodeType, TProjectionNode, TViewNode, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; -import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; +import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CHILD_HEAD, CLEANUP, FLAGS, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, T_HOST, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeType} from './node_assert'; import {renderStringify} from './util/misc_utils'; @@ -558,11 +558,12 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null { // Skip over element and ICU containers as those are represented by a comment node and // can't be used as a render parent. - const parent = getHighestElementOrICUContainer(tNode).parent; + const parent = getHighestElementOrICUContainer(tNode); + const renderParent = parent.parent; // If the parent is null, then we are inserting across views: either into an embedded view or a // component view. - if (parent == null) { + if (renderParent == null) { const hostTNode = currentView[T_HOST] !; if (hostTNode.type === TNodeType.View) { // We are inserting a root element of an embedded view We might delay insertion of children @@ -579,10 +580,17 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null { return getHostNative(currentView); } } else { - ngDevMode && assertNodeType(parent, TNodeType.Element); - if (parent.flags & TNodeFlags.isComponent) { + const isIcuCase = parent && parent.type === TNodeType.IcuContainer; + // If the parent of this node is an ICU container, then it is represented by comment node and we + // need to use it as an anchor. If it is projected then its direct parent node is the renderer. + if (isIcuCase && parent.flags & TNodeFlags.isProjected) { + return getNativeByTNode(parent, currentView).parentNode as RElement; + } + + ngDevMode && assertNodeType(renderParent, TNodeType.Element); + if (renderParent.flags & TNodeFlags.isComponent && !isIcuCase) { const tData = currentView[TVIEW].data; - const tNode = tData[parent.index] as TNode; + const tNode = tData[renderParent.index] as TNode; const encapsulation = (tData[tNode.directiveStart] as ComponentDef).encapsulation; // We've got a parent which is an element in the current view. We just need to verify if the @@ -597,7 +605,7 @@ function getRenderParent(tNode: TNode, currentView: LView): RElement|null { } } - return getNativeByTNode(parent, currentView) as RElement; + return getNativeByTNode(renderParent, currentView) as RElement; } } @@ -729,15 +737,14 @@ function getHighestElementOrICUContainer(tNode: TNode): TNode { return tNode; } -export function getBeforeNodeForView(index: number, lContainer: LContainer) { - const containerNative = lContainer[NATIVE]; - - if (index + 1 < lContainer.length - CONTAINER_HEADER_OFFSET) { - const view = lContainer[CONTAINER_HEADER_OFFSET + index + 1] as LView; - const viewTNode = view[T_HOST] as TViewNode; - return viewTNode.child ? getNativeByTNode(viewTNode.child, view) : containerNative; +export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode { + const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1; + if (nextViewIndex < lContainer.length) { + const lView = lContainer[nextViewIndex] as LView; + const tViewNodeChild = (lView[T_HOST] as TViewNode).child; + return tViewNodeChild !== null ? getNativeByTNode(tViewNodeChild, lView) : lContainer[NATIVE]; } else { - return containerNative; + return lContainer[NATIVE]; } } diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index b291587bc2..33c88df692 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -171,17 +171,18 @@ function readClassValueFromTNode(tNode: TNode): string { * Attribute matching depends upon `isInlineTemplate` and `isProjectionMode`. * The following table summarizes which types of attributes we attempt to match: * - * ========================================================================================= - * Modes | Normal Attributes | Bindings Attributes | Template Attributes - * ========================================================================================= - * Inline + Projection | YES | YES | NO - * ----------------------------------------------------------------------------------------- - * Inline + Directive | NO | NO | YES - * ----------------------------------------------------------------------------------------- - * Non-inline + Projection | YES | YES | NO - * ----------------------------------------------------------------------------------------- - * Non-inline + Directive | YES | YES | NO - * ========================================================================================= + * =========================================================================================================== + * Modes | Normal Attributes | Bindings Attributes | Template Attributes | I18n + * Attributes + * =========================================================================================================== + * Inline + Projection | YES | YES | NO | YES + * ----------------------------------------------------------------------------------------------------------- + * Inline + Directive | NO | NO | YES | NO + * ----------------------------------------------------------------------------------------------------------- + * Non-inline + Projection | YES | YES | NO | YES + * ----------------------------------------------------------------------------------------------------------- + * Non-inline + Directive | YES | YES | NO | YES + * =========================================================================================================== * * @param name the name of the attribute to find * @param attrs the attribute array to examine @@ -203,7 +204,8 @@ function findAttrIndexInNode( const maybeAttrName = attrs[i]; if (maybeAttrName === name) { return i; - } else if (maybeAttrName === AttributeMarker.Bindings) { + } else if ( + maybeAttrName === AttributeMarker.Bindings || maybeAttrName === AttributeMarker.I18n) { bindingsMode = true; } else if (maybeAttrName === AttributeMarker.Classes) { let value = attrs[++i]; @@ -255,29 +257,6 @@ export function getProjectAsAttrValue(tNode: TNode): CssSelector|null { return null; } -/** - * Checks a given node against matching projection selectors and returns - * selector index (or 0 if none matched). - * - * This function takes into account the parsed ngProjectAs selector from the node's attributes. - * If present, it will check whether the ngProjectAs selector matches any of the projection - * selectors. - */ -export function matchingProjectionSelectorIndex( - tNode: TNode, selectors: CssSelectorList[]): number { - const ngProjectAsAttrVal = getProjectAsAttrValue(tNode); - for (let i = 0; i < selectors.length; i++) { - // If we ran into an `ngProjectAs` attribute, we should match its parsed selector - // to the list of selectors, otherwise we fall back to matching against the node. - if (ngProjectAsAttrVal === null ? - isNodeMatchingSelectorList(tNode, selectors[i], /* isProjectionMode */ true) : - isSelectorInSelectorList(ngProjectAsAttrVal, selectors[i])) { - return i + 1; // first matching selector "captures" a given node - } - } - return 0; -} - function getNameOnlyMarkerIndex(nodeAttrs: TAttributes) { for (let i = 0; i < nodeAttrs.length; i++) { const nodeAttr = nodeAttrs[i]; @@ -305,7 +284,7 @@ function matchTemplateAttribute(attrs: TAttributes, name: string): number { * @param selector Selector to be checked. * @param list List in which to look for the selector. */ -function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean { +export function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean { selectorListLoop: for (let i = 0; i < list.length; i++) { const currentSelectorInList = list[i]; if (selector.length !== currentSelectorInList.length) { diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index 82d5572526..81bb75bab1 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -18,14 +18,15 @@ import {assertDataInRange, assertDefined, assertEqual} from '../util/assert'; import {assertPreviousIsParent} from './assert'; import {getNodeInjectable, locateDirectiveOrProvider} from './di'; import {NG_ELEMENT_ID} from './fields'; -import {store, ɵɵload} from './instructions/all'; +import {store} from './instructions/all'; import {storeCleanupWithContext} from './instructions/shared'; import {unusedValueExportToPlacateAjd as unused1} from './interfaces/definition'; import {unusedValueExportToPlacateAjd as unused2} from './interfaces/injector'; import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, unusedValueExportToPlacateAjd as unused3} from './interfaces/node'; import {LQueries, unusedValueExportToPlacateAjd as unused4} from './interfaces/query'; -import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW} from './interfaces/view'; -import {getCurrentQueryIndex, getIsParent, getLView, isCreationMode, setCurrentQueryIndex} from './state'; +import {CONTENT_QUERIES, HEADER_OFFSET, LView, QUERIES, TVIEW, TView} from './interfaces/view'; +import {getCurrentQueryIndex, getIsParent, getLView, getPreviousOrParentTNode, isCreationMode, setCurrentQueryIndex} from './state'; +import {isContentQueryHost, loadInternal} from './util/view_utils'; import {createElementRef, createTemplateRef} from './view_engine_compatibility'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4; @@ -57,52 +58,56 @@ export interface QueryPredicate { * - values collected based on a predicate * - `QueryList` to which collected values should be reported */ -export interface LQuery { - /** - * Next query. Used when queries are stored as a linked list in `LQueries`. - */ - next: LQuery|null; +class LQuery { + constructor( + /** + * Next query. Used when queries are stored as a linked list in `LQueries`. + */ + public next: LQuery|null, - /** - * Destination to which the value should be added. - */ - list: QueryList; + /** + * Destination to which the value should be added. + */ + public list: QueryList, - /** - * A predicate which determines if a given element/directive should be included in the query - * results. - */ - predicate: QueryPredicate; + /** + * A predicate which determines if a given element/directive should be included in the query + * results. + */ + public predicate: QueryPredicate, - /** - * Values which have been located. - * - * This is what builds up the `QueryList._valuesTree`. - */ - values: any[]; + /** + * Values which have been located. + * This is what builds up the `QueryList._valuesTree`. + */ + public values: any[], - /** - * A pointer to an array that stores collected values from views. This is necessary so we know a - * container into which to insert nodes collected from views. - */ - containerValues: any[]|null; + /** + * A pointer to an array that stores collected values from views. This is necessary so we + * know a container into which to insert nodes collected from views. + */ + public containerValues: any[]|null) {} } export class LQueries_ implements LQueries { constructor( public parent: LQueries_|null, private shallow: LQuery|null, - private deep: LQuery|null) {} + private deep: LQuery|null, public nodeIndex: number = -1) {} track(queryList: QueryList, predicate: Type|string[], descend?: boolean, read?: Type): void { if (descend) { - this.deep = createQuery(this.deep, queryList, predicate, read != null ? read : null); + this.deep = createLQuery(this.deep, queryList, predicate, read != null ? read : null); } else { - this.shallow = createQuery(this.shallow, queryList, predicate, read != null ? read : null); + this.shallow = createLQuery(this.shallow, queryList, predicate, read != null ? read : null); } } - clone(): LQueries { return new LQueries_(this, null, this.deep); } + clone(tNode: TNode): LQueries { + return this.shallow !== null || isContentQueryHost(tNode) ? + new LQueries_(this, null, this.deep, tNode.index) : + this; + } container(): LQueries|null { const shallowResults = copyQueriesToContainer(this.shallow); @@ -144,14 +149,7 @@ function copyQueriesToContainer(query: LQuery| null): LQuery|null { while (query) { const containerValues: any[] = []; // prepare room for views query.values.push(containerValues); - const clonedQuery: LQuery = { - next: result, - list: query.list, - predicate: query.predicate, - values: containerValues, - containerValues: null - }; - result = clonedQuery; + result = new LQuery(result, query.list, query.predicate, containerValues, null); query = query.next; } @@ -162,14 +160,7 @@ function copyQueriesToView(query: LQuery| null): LQuery|null { let result: LQuery|null = null; while (query) { - const clonedQuery: LQuery = { - next: result, - list: query.list, - predicate: query.predicate, - values: [], - containerValues: query.values - }; - result = clonedQuery; + result = new LQuery(result, query.list, query.predicate, [], query.values); query = query.next; } @@ -330,10 +321,9 @@ function add( function addMatch(query: LQuery, matchingValue: any, insertBeforeViewMatches: boolean): void { // Views created in constructors may have their container values created too early. In this case, - // ensure template node results are spliced before container results. Otherwise, results inside + // ensure template node results are unshifted before container results. Otherwise, results inside // embedded views will appear before results on parent template nodes when flattened. - insertBeforeViewMatches ? query.values.splice(-1, 0, matchingValue) : - query.values.push(matchingValue); + insertBeforeViewMatches ? query.values.unshift(matchingValue) : query.values.push(matchingValue); query.list.setDirty(); } @@ -346,37 +336,33 @@ function createPredicate(predicate: Type| string[], read: Type| null): }; } -function createQuery( +function createLQuery( previous: LQuery| null, queryList: QueryList, predicate: Type| string[], read: Type| null): LQuery { - return { - next: previous, - list: queryList, - predicate: createPredicate(predicate, read), - values: (queryList as any as QueryList_)._valuesTree, - containerValues: null - }; + return new LQuery( + previous, queryList, createPredicate(predicate, read), + (queryList as any as QueryList_)._valuesTree, null); } type QueryList_ = QueryList& {_valuesTree: any[], _static: boolean}; /** - * Creates and returns a QueryList. + * Creates a QueryList and stores it in LView's collection of active queries (LQueries). * * @param predicate The type for which the query will search * @param descend Whether or not to descend into children * @param read What to save in the query * @returns QueryList */ -export function query( +function createQueryListInLView( // TODO: "read" should be an AbstractType (FW-486) - predicate: Type| string[], descend: boolean, read: any): QueryList { + lView: LView, predicate: Type| string[], descend: boolean, read: any, isStatic: boolean, + nodeIndex: number): QueryList { ngDevMode && assertPreviousIsParent(getIsParent()); - const lView = getLView(); const queryList = new QueryList() as QueryList_; - const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null)); + const queries = lView[QUERIES] || (lView[QUERIES] = new LQueries_(null, null, null, nodeIndex)); queryList._valuesTree = []; - queryList._static = false; + queryList._static = isStatic; queries.track(queryList, predicate, descend, read); storeCleanupWithContext(lView, queryList, queryList.destroy); return queryList; @@ -416,12 +402,10 @@ export function ɵɵqueryRefresh(queryList: QueryList): boolean { export function ɵɵstaticViewQuery( // TODO(FW-486): "read" should be an AbstractType predicate: Type| string[], descend: boolean, read: any): void { - const queryList = ɵɵviewQuery(predicate, descend, read) as QueryList_; - const tView = getLView()[TVIEW]; - queryList._static = true; - if (!tView.staticViewQueries) { - tView.staticViewQueries = true; - } + const lView = getLView(); + const tView = lView[TVIEW]; + viewQueryInternal(lView, tView, predicate, descend, read, true); + tView.staticViewQueries = true; } /** @@ -439,14 +423,21 @@ export function ɵɵviewQuery( predicate: Type| string[], descend: boolean, read: any): QueryList { const lView = getLView(); const tView = lView[TVIEW]; + return viewQueryInternal(lView, tView, predicate, descend, read, false); +} + +function viewQueryInternal( + lView: LView, tView: TView, predicate: Type| string[], descend: boolean, read: any, + isStatic: boolean): QueryList { if (tView.firstTemplatePass) { tView.expandoStartIndex++; } const index = getCurrentQueryIndex(); - const viewQuery: QueryList = query(predicate, descend, read); - store(index - HEADER_OFFSET, viewQuery); + const queryList: QueryList = + createQueryListInLView(lView, predicate, descend, read, isStatic, -1); + store(index - HEADER_OFFSET, queryList); setCurrentQueryIndex(index + 1); - return viewQuery; + return queryList; } /** @@ -457,7 +448,7 @@ export function ɵɵviewQuery( export function ɵɵloadViewQuery(): T { const index = getCurrentQueryIndex(); setCurrentQueryIndex(index + 1); - return ɵɵload(index - HEADER_OFFSET); + return loadInternal(getLView(), index - HEADER_OFFSET); } /** @@ -478,7 +469,18 @@ export function ɵɵcontentQuery( read: any): QueryList { const lView = getLView(); const tView = lView[TVIEW]; - const contentQuery: QueryList = query(predicate, descend, read); + const tNode = getPreviousOrParentTNode(); + return contentQueryInternal( + lView, tView, directiveIndex, predicate, descend, read, false, tNode.index); +} + +function contentQueryInternal( + lView: LView, tView: TView, directiveIndex: number, predicate: Type| string[], + descend: boolean, + // TODO(FW-486): "read" should be an AbstractType + read: any, isStatic: boolean, nodeIndex: number): QueryList { + const contentQuery: QueryList = + createQueryListInLView(lView, predicate, descend, read, isStatic, nodeIndex); (lView[CONTENT_QUERIES] || (lView[CONTENT_QUERIES] = [])).push(contentQuery); if (tView.firstTemplatePass) { const tViewContentQueries = tView.contentQueries || (tView.contentQueries = []); @@ -507,12 +509,11 @@ export function ɵɵstaticContentQuery( directiveIndex: number, predicate: Type| string[], descend: boolean, // TODO(FW-486): "read" should be an AbstractType read: any): void { - const queryList = ɵɵcontentQuery(directiveIndex, predicate, descend, read) as QueryList_; - const tView = getLView()[TVIEW]; - queryList._static = true; - if (!tView.staticContentQueries) { - tView.staticContentQueries = true; - } + const lView = getLView(); + const tView = lView[TVIEW]; + const tNode = getPreviousOrParentTNode(); + contentQueryInternal(lView, tView, directiveIndex, predicate, descend, read, true, tNode.index); + tView.staticContentQueries = true; } /** diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index a0ecfa2b76..582d77e7bc 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -245,6 +245,27 @@ export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) { Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition); } +/** + * Returns he current depth of the super/sub class inheritance chain. + * + * This will return how many inherited directive/component classes + * exist in the current chain. + * + * ```typescript + * @Directive({ selector: '[super-dir]' }) + * class SuperDir {} + * + * @Directive({ selector: '[sub-dir]' }) + * class SubDir extends SuperDir {} + * + * // if `
` is used then the super class height is `1` + * // if `
` is used then the super class height is `0` + * ``` + */ +export function getActiveDirectiveSuperClassHeight() { + return activeDirectiveSuperClassHeight; +} + /** * Returns the current super class (reverse inheritance) depth for a directive. * @@ -282,8 +303,9 @@ export function getPreviousOrParentTNode(): TNode { return previousOrParentTNode; } -export function setPreviousOrParentTNode(tNode: TNode) { +export function setPreviousOrParentTNode(tNode: TNode, _isParent: boolean) { previousOrParentTNode = tNode; + isParent = _isParent; } export function setTNodeAndViewData(tNode: TNode, view: LView) { @@ -304,8 +326,11 @@ export function getIsParent(): boolean { return isParent; } -export function setIsParent(value: boolean): void { - isParent = value; +export function setIsNotParent(): void { + isParent = false; +} +export function setIsParent(): void { + isParent = true; } diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index 7f08b2748a..23881b4caa 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -5,7 +5,7 @@ * 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; +import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {AttributeMarker, TAttributes} from '../interfaces/node'; import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; @@ -943,7 +943,9 @@ function updateSingleStylingValue( if (currDirective !== directiveIndex) { const prop = getProp(context, singleIndex); const sanitizer = getStyleSanitizer(context, directiveIndex); - setSanitizeFlag(context, singleIndex, (sanitizer && sanitizer(prop)) ? true : false); + setSanitizeFlag( + context, singleIndex, + (sanitizer && sanitizer(prop, null, StyleSanitizeMode.ValidateProperty)) ? true : false); } // the value will always get updated (even if the dirty flag is skipped) @@ -1141,7 +1143,8 @@ export function setStyle( native: any, prop: string, value: string | null, renderer: Renderer3, sanitizer: StyleSanitizeFn | null, store?: BindingStore | null, playerBuilder?: ClassAndStylePlayerBuilder| null) { - value = sanitizer && value ? sanitizer(prop, value) : value; + value = + sanitizer && value ? sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) : value; if (store || playerBuilder) { if (store) { store.setValue(prop, value); @@ -1461,7 +1464,9 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) { function prepareInitialFlag( context: StylingContext, prop: string, entryIsClassBased: boolean, sanitizer?: StyleSanitizeFn | null) { - let flag = (sanitizer && sanitizer(prop)) ? StylingFlags.Sanitize : StylingFlags.None; + let flag = (sanitizer && sanitizer(prop, null, StyleSanitizeMode.ValidateProperty)) ? + StylingFlags.Sanitize : + StylingFlags.None; let initialIndex: number; if (entryIsClassBased) { @@ -1636,7 +1641,7 @@ function diffSummaryValues(result: any[], name: string, prop: string, a: any, b: } } -function getSinglePropIndexValue( +export function getSinglePropIndexValue( context: StylingContext, directiveIndex: number, offset: number, isClassBased: boolean) { const singlePropOffsetRegistryIndex = context[StylingIndex.DirectiveRegistryPosition] @@ -1747,7 +1752,7 @@ export function getInitialStyleStringValue(context: StylingContext): string { } /** - * Returns the current cached mutli-value for a given directiveIndex within the provided context. + * Returns the current cached multi-value for a given directiveIndex within the provided context. */ function readCachedMapValue( context: StylingContext, entryIsClassBased: boolean, directiveIndex: number) { diff --git a/packages/core/src/render3/styling/host_instructions_queue.ts b/packages/core/src/render3/styling/host_instructions_queue.ts index a691397fb4..e5045cd9e7 100644 --- a/packages/core/src/render3/styling/host_instructions_queue.ts +++ b/packages/core/src/render3/styling/host_instructions_queue.ts @@ -35,9 +35,12 @@ export function registerHostDirective(context: StylingContext, directiveIndex: n */ export function enqueueHostInstruction( context: StylingContext, priority: number, instructionFn: T, instructionFnArgs: ParamsOf) { - const buffer: HostInstructionsQueue = context[StylingIndex.HostInstructionsQueue] !; - const index = findNextInsertionIndex(buffer, priority); - buffer.splice(index, 0, priority, instructionFn, instructionFnArgs); + const buffer: HostInstructionsQueue|null = context[StylingIndex.HostInstructionsQueue]; + // Buffer may be null if host element is a template node. In this case, just ignore the style. + if (buffer != null) { + const index = findNextInsertionIndex(buffer, priority); + buffer.splice(index, 0, priority, instructionFn, instructionFnArgs); + } } /** diff --git a/packages/core/src/render3/styling_next/bindings.ts b/packages/core/src/render3/styling_next/bindings.ts new file mode 100644 index 0000000000..728a64f2d3 --- /dev/null +++ b/packages/core/src/render3/styling_next/bindings.ts @@ -0,0 +1,514 @@ +/** +* @license +* Copyright Google Inc. 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 {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; +import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer'; + +import {ApplyStylingFn, LStylingData, LStylingMap, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; +import {allowStylingFlush, getBindingValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasValueChanged, isContextLocked, isSanitizationRequired, isStylingValueDefined, lockContext, setGuardMask} from './util'; + + +/** + * -------- + * + * This file contains the core logic for styling in Angular. + * + * All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`) + * will have their values be applied through the logic in this file. + * + * When a binding is encountered (e.g. `
`) then + * the binding data will be populated into a `TStylingContext` data-structure. + * There is only one `TStylingContext` per `TNode` and each element instance + * will update its style/class binding values in concert with the styling + * context. + * + * To learn more about the algorithm see `TStylingContext`. + * + * -------- + */ + +const DEFAULT_BINDING_VALUE = null; +const DEFAULT_SIZE_VALUE = 1; + +// The first bit value reflects a map-based binding value's bit. +// The reason why it's always activated for every entry in the map +// is so that if any map-binding values update then all other prop +// based bindings will pass the guard check automatically without +// any extra code or flags. +export const DEFAULT_GUARD_MASK_VALUE = 0b1; +const STYLING_INDEX_FOR_MAP_BINDING = 0; +const STYLING_INDEX_START_VALUE = 1; + +// the values below are global to all styling code below. Each value +// will either increment or mutate each time a styling instruction is +// executed. Do not modify the values below. +let currentStyleIndex = STYLING_INDEX_START_VALUE; +let currentClassIndex = STYLING_INDEX_START_VALUE; +let stylesBitMask = 0; +let classesBitMask = 0; +let deferredBindingQueue: (TStylingContext | number | string | null | boolean)[] = []; + +/** + * Visits a class-based binding and updates the new value (if changed). + * + * This function is called each time a class-based styling instruction + * is executed. It's important that it's always called (even if the value + * has not changed) so that the inner counter index value is incremented. + * This way, each instruction is always guaranteed to get the same counter + * state each time it's called (which then allows the `TStylingContext` + * and the bit mask values to be in sync). + */ +export function updateClassBinding( + context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number, + value: boolean | string | null | undefined | LStylingMap, deferRegistration: boolean, + forceUpdate: boolean): void { + const isMapBased = !prop; + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentClassIndex++; + const updated = updateBindingData( + context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, false); + if (updated || forceUpdate) { + classesBitMask |= 1 << index; + } +} + +/** + * Visits a style-based binding and updates the new value (if changed). + * + * This function is called each time a style-based styling instruction + * is executed. It's important that it's always called (even if the value + * has not changed) so that the inner counter index value is incremented. + * This way, each instruction is always guaranteed to get the same counter + * state each time it's called (which then allows the `TStylingContext` + * and the bit mask values to be in sync). + */ +export function updateStyleBinding( + context: TStylingContext, data: LStylingData, prop: string | null, bindingIndex: number, + value: String | string | number | null | undefined | LStylingMap, + sanitizer: StyleSanitizeFn | null, deferRegistration: boolean, forceUpdate: boolean): void { + const isMapBased = !prop; + const index = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : currentStyleIndex++; + const sanitizationRequired = isMapBased ? + true : + (sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false); + const updated = updateBindingData( + context, data, index, prop, bindingIndex, value, deferRegistration, forceUpdate, + sanitizationRequired); + if (updated || forceUpdate) { + stylesBitMask |= 1 << index; + } +} + +/** + * Called each time a binding value has changed within the provided `TStylingContext`. + * + * This function is designed to be called from `updateStyleBinding` and `updateClassBinding`. + * If called during the first update pass, the binding will be registered in the context. + * If the binding does get registered and the `deferRegistration` flag is true then the + * binding data will be queued up until the context is later flushed in `applyStyling`. + * + * This function will also update binding slot in the provided `LStylingData` with the + * new binding entry (if it has changed). + * + * @returns whether or not the binding value was updated in the `LStylingData`. + */ +function updateBindingData( + context: TStylingContext, data: LStylingData, counterIndex: number, prop: string | null, + bindingIndex: number, + value: string | String | number | boolean | null | undefined | LStylingMap, + deferRegistration: boolean, forceUpdate: boolean, sanitizationRequired: boolean): boolean { + if (!isContextLocked(context)) { + if (deferRegistration) { + deferBindingRegistration(context, counterIndex, prop, bindingIndex, sanitizationRequired); + } else { + deferredBindingQueue.length && flushDeferredBindings(); + + // this will only happen during the first update pass of the + // context. The reason why we can't use `tNode.firstTemplatePass` + // here is because its not guaranteed to be true when the first + // update pass is executed (remember that all styling instructions + // are run in the update phase, and, as a result, are no more + // styling instructions that are run in the creation phase). + registerBinding(context, counterIndex, prop, bindingIndex, sanitizationRequired); + } + } + + const changed = forceUpdate || hasValueChanged(data[bindingIndex], value); + if (changed) { + data[bindingIndex] = value; + } + return changed; +} + +/** + * Schedules a binding registration to be run at a later point. + * + * The reasoning for this feature is to ensure that styling + * bindings are registered in the correct order for when + * directives/components have a super/sub class inheritance + * chains. Each directive's styling bindings must be + * registered into the context in reverse order. Therefore all + * bindings will be buffered in reverse order and then applied + * after the inheritance chain exits. + */ +function deferBindingRegistration( + context: TStylingContext, counterIndex: number, prop: string | null, bindingIndex: number, + sanitizationRequired: boolean) { + deferredBindingQueue.unshift(context, counterIndex, prop, bindingIndex, sanitizationRequired); +} + +/** + * Flushes the collection of deferred bindings and causes each entry + * to be registered into the context. + */ +function flushDeferredBindings() { + let i = 0; + while (i < deferredBindingQueue.length) { + const context = deferredBindingQueue[i++] as TStylingContext; + const count = deferredBindingQueue[i++] as number; + const prop = deferredBindingQueue[i++] as string; + const bindingIndex = deferredBindingQueue[i++] as number | null; + const sanitizationRequired = deferredBindingQueue[i++] as boolean; + registerBinding(context, count, prop, bindingIndex, sanitizationRequired); + } + deferredBindingQueue.length = 0; +} + +/** + * Registers the provided binding (prop + bindingIndex) into the context. + * + * This function is shared between bindings that are assigned immediately + * (via `updateBindingData`) and at a deferred stage. When called, it will + * figure out exactly where to place the binding data in the context. + * + * It is needed because it will either update or insert a styling property + * into the context at the correct spot. + * + * When called, one of two things will happen: + * + * 1) If the property already exists in the context then it will just add + * the provided `bindingValue` to the end of the binding sources region + * for that particular property. + * + * - If the binding value is a number then it will be added as a new + * binding index source next to the other binding sources for the property. + * + * - Otherwise, if the binding value is a string/boolean/null type then it will + * replace the default value for the property if the default value is `null`. + * + * 2) If the property does not exist then it will be inserted into the context. + * The styling context relies on all properties being stored in alphabetical + * order, so it knows exactly where to store it. + * + * When inserted, a default `null` value is created for the property which exists + * as the default value for the binding. If the bindingValue property is inserted + * and it is either a string, number or null value then that will replace the default + * value. + * + * Note that this function is also used for map-based styling bindings. They are treated + * much the same as prop-based bindings, but, because they do not have a property value + * (since it's a map), all map-based entries are stored in an already populated area of + * the context at the top (which is reserved for map-based entries). + */ +export function registerBinding( + context: TStylingContext, countId: number, prop: string | null, + bindingValue: number | null | string | boolean, sanitizationRequired?: boolean) { + // prop-based bindings (e.g `
`) + if (prop) { + let found = false; + let i = getPropValuesStartPosition(context); + while (i < context.length) { + const valuesCount = getValuesCount(context, i); + const p = getProp(context, i); + found = prop <= p; + if (found) { + // all style/class bindings are sorted by property name + if (prop < p) { + allocateNewContextEntry(context, i, prop, sanitizationRequired); + } + addBindingIntoContext(context, false, i, bindingValue, countId); + break; + } + i += TStylingContextIndex.BindingsStartOffset + valuesCount; + } + + if (!found) { + allocateNewContextEntry(context, context.length, prop, sanitizationRequired); + addBindingIntoContext(context, false, i, bindingValue, countId); + } + } else { + // map-based bindings (e.g `
`) + // there is no need to allocate the map-based binding region into the context + // since it is already there when the context is first created. + addBindingIntoContext( + context, true, TStylingContextIndex.MapBindingsPosition, bindingValue, countId); + } +} + +function allocateNewContextEntry( + context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean) { + // 1,2: splice index locations + // 3: each entry gets a config value (guard mask + flags) + // 4. each entry gets a size value (which is always one because there is always a default binding + // value) + // 5. the property that is getting allocated into the context + // 6. the default binding value (usually `null`) + const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired : + TStylingContextPropConfigFlags.Default; + context.splice(index, 0, config, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE); + setGuardMask(context, index, DEFAULT_GUARD_MASK_VALUE); +} + +/** + * Inserts a new binding value into a styling property tuple in the `TStylingContext`. + * + * A bindingValue is inserted into a context during the first update pass + * of a template or host bindings function. When this occurs, two things + * happen: + * + * - If the bindingValue value is a number then it is treated as a bindingIndex + * value (a index in the `LView`) and it will be inserted next to the other + * binding index entries. + * + * - Otherwise the binding value will update the default value for the property + * and this will only happen if the default value is `null`. + * + * Note that this function also handles map-based bindings and will insert them + * at the top of the context. + */ +function addBindingIntoContext( + context: TStylingContext, isMapBased: boolean, index: number, + bindingValue: number | string | boolean | null, countId: number) { + const valuesCount = getValuesCount(context, index); + + let lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount; + if (!isMapBased) { + // prop-based values all have default values, but map-based entries do not. + // we want to access the index for the default value in this case and not just + // the bindings... + lastValueIndex--; + } + + if (typeof bindingValue === 'number') { + context.splice(lastValueIndex, 0, bindingValue); + (context[index + TStylingContextIndex.ValuesCountOffset] as number)++; + + // now that a new binding index has been added to the property + // the guard mask bit value (at the `countId` position) needs + // to be included into the existing mask value. + const guardMask = getGuardMask(context, index) | (1 << countId); + setGuardMask(context, index, guardMask); + } else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) { + context[lastValueIndex] = bindingValue; + } +} + +/** + * Applies all class entries in the provided context to the provided element and resets + * any counter and/or bitMask values associated with class bindings. + * + * @returns whether or not the classes were flushed to the element. + */ +export function applyClasses( + renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext, + element: RElement, directiveIndex: number): boolean { + let classesFlushed = false; + if (allowStylingFlush(context, directiveIndex)) { + const isFirstPass = !isContextLocked(context); + isFirstPass && lockContext(context); + if (classesBitMask) { + // there is no way to sanitize a class value therefore `sanitizer=null` + applyStyling(context, renderer, element, data, classesBitMask, setClass, null); + classesBitMask = 0; + classesFlushed = true; + } + currentClassIndex = STYLING_INDEX_START_VALUE; + } + return classesFlushed; +} + +/** + * Applies all style entries in the provided context to the provided element and resets + * any counter and/or bitMask values associated with style bindings. + * + * @returns whether or not the styles were flushed to the element. + */ +export function applyStyles( + renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData, context: TStylingContext, + element: RElement, directiveIndex: number, sanitizer: StyleSanitizeFn | null): boolean { + let stylesFlushed = false; + if (allowStylingFlush(context, directiveIndex)) { + const isFirstPass = !isContextLocked(context); + isFirstPass && lockContext(context); + if (stylesBitMask) { + applyStyling(context, renderer, element, data, stylesBitMask, setStyle, sanitizer); + stylesBitMask = 0; + stylesFlushed = true; + } + currentStyleIndex = STYLING_INDEX_START_VALUE; + return true; + } + return stylesFlushed; +} + +/** + * Runs through the provided styling context and applies each value to + * the provided element (via the renderer) if one or more values are present. + * + * This function will iterate over all entries present in the provided + * `TStylingContext` array (both prop-based and map-based bindings).- + * + * Each entry, within the `TStylingContext` array, is stored alphabetically + * and this means that each prop/value entry will be applied in order + * (so long as it is marked dirty in the provided `bitMask` value). + * + * If there are any map-based entries present (which are applied to the + * element via the `[style]` and `[class]` bindings) then those entries + * will be applied as well. However, the code for that is not apart of + * this function. Instead, each time a property is visited, then the + * code below will call an external function called `stylingMapsSyncFn` + * and, if present, it will keep the application of styling values in + * map-based bindings up to sync with the application of prop-based + * bindings. + * + * Visit `styling_next/map_based_bindings.ts` to learn more about how the + * algorithm works for map-based styling bindings. + * + * Note that this function is not designed to be called in isolation (use + * `applyClasses` and `applyStyles` to actually apply styling values). + */ +export function applyStyling( + context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, + bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn, + sanitizer: StyleSanitizeFn | null) { + deferredBindingQueue.length && flushDeferredBindings(); + + const bitMask = normalizeBitMaskValue(bitMaskValue); + const stylingMapsSyncFn = getStylingMapsSyncFn(); + const mapsGuardMask = getGuardMask(context, TStylingContextIndex.MapBindingsPosition); + const applyAllValues = (bitMask & mapsGuardMask) > 0; + const mapsMode = + applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues; + + let i = getPropValuesStartPosition(context); + while (i < context.length) { + const valuesCount = getValuesCount(context, i); + const guardMask = getGuardMask(context, i); + if (bitMask & guardMask) { + let valueApplied = false; + const prop = getProp(context, i); + const valuesCountUpToDefault = valuesCount - 1; + const defaultValue = getBindingValue(context, i, valuesCountUpToDefault) as string | null; + + // case 1: apply prop-based values + // try to apply the binding values and see if a non-null + // value gets set for the styling binding + for (let j = 0; j < valuesCountUpToDefault; j++) { + const bindingIndex = getBindingValue(context, i, j) as number; + const value = bindingData[bindingIndex]; + if (isStylingValueDefined(value)) { + const finalValue = sanitizer && isSanitizationRequired(context, i) ? + sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) : + value; + applyStylingFn(renderer, element, prop, finalValue, bindingIndex); + valueApplied = true; + break; + } + } + + // case 2: apply map-based values + // traverse through each map-based styling binding and update all values up to + // the provided `prop` value. If the property was not applied in the loop above + // then it will be attempted to be applied in the maps sync code below. + if (stylingMapsSyncFn) { + // determine whether or not to apply the target property or to skip it + const mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp : + StylingMapsSyncMode.ApplyTargetProp); + const valueAppliedWithinMap = stylingMapsSyncFn( + context, renderer, element, bindingData, applyStylingFn, sanitizer, mode, prop, + defaultValue); + valueApplied = valueApplied || valueAppliedWithinMap; + } + + // case 3: apply the default value + // if the value has not yet been applied then a truthy value does not exist in the + // prop-based or map-based bindings code. If and when this happens, just apply the + // default value (even if the default value is `null`). + if (!valueApplied) { + applyStylingFn(renderer, element, prop, defaultValue); + } + } + + i += TStylingContextIndex.BindingsStartOffset + valuesCount; + } + + // the map-based styling entries may have not applied all their + // values. For this reason, one more call to the sync function + // needs to be issued at the end. + if (stylingMapsSyncFn) { + stylingMapsSyncFn(context, renderer, element, bindingData, applyStylingFn, sanitizer, mapsMode); + } +} + +function normalizeBitMaskValue(value: number | boolean): number { + // if pass => apply all values (-1 implies that all bits are flipped to true) + if (value === true) return -1; + + // if pass => skip all values + if (value === false) return 0; + + // return the bit mask value as is + return value; +} + +let _activeStylingMapApplyFn: SyncStylingMapsFn|null = null; +export function getStylingMapsSyncFn() { + return _activeStylingMapApplyFn; +} + +export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) { + _activeStylingMapApplyFn = fn; +} + +/** + * Assigns a style value to a style property for the given element. + */ +const setStyle: ApplyStylingFn = + (renderer: Renderer3 | null, native: any, prop: string, value: string | null) => { + if (value) { + // opacity, z-index and flexbox all have number values + // and these need to be converted into strings so that + // they can be assigned properly. + value = value.toString(); + ngDevMode && ngDevMode.rendererSetStyle++; + renderer && isProceduralRenderer(renderer) ? + renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : + native.style.setProperty(prop, value); + } else { + ngDevMode && ngDevMode.rendererRemoveStyle++; + renderer && isProceduralRenderer(renderer) ? + renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) : + native.style.removeProperty(prop); + } + }; + +/** + * Adds/removes the provided className value to the provided element. + */ +const setClass: ApplyStylingFn = + (renderer: Renderer3 | null, native: any, className: string, value: any) => { + if (className !== '') { + if (value) { + ngDevMode && ngDevMode.rendererAddClass++; + renderer && isProceduralRenderer(renderer) ? renderer.addClass(native, className) : + native.classList.add(className); + } else { + ngDevMode && ngDevMode.rendererRemoveClass++; + renderer && isProceduralRenderer(renderer) ? renderer.removeClass(native, className) : + native.classList.remove(className); + } + } + }; diff --git a/packages/core/src/render3/styling_next/instructions.ts b/packages/core/src/render3/styling_next/instructions.ts new file mode 100644 index 0000000000..f565579a2b --- /dev/null +++ b/packages/core/src/render3/styling_next/instructions.ts @@ -0,0 +1,319 @@ +/** +* @license +* Copyright Google Inc. 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 {Sanitizer} from '../../sanitization/security'; +import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; +import {LContainer} from '../interfaces/container'; +import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node'; +import {RElement} from '../interfaces/renderer'; +import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling'; +import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER, SANITIZER} from '../interfaces/view'; +import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state'; +import {NO_CHANGE} from '../tokens'; +import {renderStringify} from '../util/misc_utils'; +import {getTNode, isStylingContext as isOldStylingContext} from '../util/view_utils'; + +import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings'; +import {TStylingContext} from './interfaces'; +import {activeStylingMapFeature, normalizeIntoStylingMap} from './map_based_bindings'; +import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state'; +import {attachStylingDebugObject} from './styling_debug'; +import {allocTStylingContext, getCurrentOrLViewSanitizer, hasValueChanged, updateContextDirectiveIndex} from './util'; + + + +/** + * -------- + * + * This file contains the core logic for how styling instructions are processed in Angular. + * + * To learn more about the algorithm see `TStylingContext`. + * + * -------- + */ + +/** + * Temporary function to bridge styling functionality between this new + * refactor (which is here inside of `styling_next/`) and the old + * implementation (which lives inside of `styling/`). + * + * This function is executed during the creation block of an element. + * Because the existing styling implementation issues a call to the + * `styling()` instruction, this instruction will also get run. The + * central idea here is that the directive index values are bound + * into the context. The directive index is temporary and is only + * required until the `select(n)` instruction is fully functional. + */ +export function stylingInit() { + const lView = getLView(); + const index = getSelectedIndex(); + const tNode = getTNode(index, lView); + updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex()); +} + +/** + * Sets the current style sanitizer function which will then be used + * within all follow-up prop and map-based style binding instructions + * for the given element. + * + * Note that once styling has been applied to the element (i.e. once + * `select(n)` is executed or the hostBindings/template function exits) + * then the active `sanitizerFn` will be set to `null`. This means that + * once styling is applied to another element then a another call to + * `styleSanitizer` will need to be made. + * + * @param sanitizerFn The sanitization function that will be used to + * process style prop/value entries. + * + * @codeGenApi + */ +export function styleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null): void { + setCurrentStyleSanitizer(sanitizer); +} + +/** + * Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`). + */ +export function styleProp( + prop: string, value: string | number | String | null, suffix?: string | null): void { + _stylingProp(prop, resolveStylePropValue(value, suffix), false); +} + +/** + * Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`). + */ +export function classProp(className: string, value: boolean | null): void { + _stylingProp(className, value, true); +} + +/** + * Shared function used to update a prop-based styling binding for an element. + */ +function _stylingProp( + prop: string, value: boolean | number | String | string | null, isClassBased: boolean) { + const index = getSelectedIndex(); + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]++; + const tNode = getTNode(index, lView); + const defer = getActiveDirectiveSuperClassHeight() > 0; + if (isClassBased) { + updateClassBinding( + getClassesContext(tNode), lView, prop, bindingIndex, value as string | boolean | null, + defer, false); + } else { + const sanitizer = getCurrentOrLViewSanitizer(lView); + updateStyleBinding( + getStylesContext(tNode), lView, prop, bindingIndex, value as string | null, sanitizer, + defer, false); + } +} + +/** + * Mirror implementation of the `styleMap()` instruction (found in `instructions/styling.ts`). + */ +export function styleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void { + _stylingMap(styles, false); +} + +/** + * Mirror implementation of the `classMap()` instruction (found in `instructions/styling.ts`). + */ +export function classMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void { + _stylingMap(classes, true); +} + +/** + * Shared function used to update a map-based styling binding for an element. + * + * When this function is called it will activate support for `[style]` and + * `[class]` bindings in Angular. + */ +function _stylingMap(value: {[key: string]: any} | string | null, isClassBased: boolean) { + activeStylingMapFeature(); + const index = getSelectedIndex(); + const lView = getLView(); + const bindingIndex = lView[BINDING_INDEX]++; + + if (value !== NO_CHANGE) { + const tNode = getTNode(index, lView); + const defer = getActiveDirectiveSuperClassHeight() > 0; + const oldValue = lView[bindingIndex]; + const valueHasChanged = hasValueChanged(oldValue, value); + const lStylingMap = normalizeIntoStylingMap(oldValue, value); + if (isClassBased) { + updateClassBinding( + getClassesContext(tNode), lView, null, bindingIndex, lStylingMap, defer, valueHasChanged); + } else { + const sanitizer = getCurrentOrLViewSanitizer(lView); + updateStyleBinding( + getStylesContext(tNode), lView, null, bindingIndex, lStylingMap, sanitizer, defer, + valueHasChanged); + } + } +} + +/** + * Temporary function to bridge styling functionality between this new + * refactor (which is here inside of `styling_next/`) and the old + * implementation (which lives inside of `styling/`). + * + * The new styling refactor ensures that styling flushing is called + * automatically when a template function exits or a follow-up element + * is visited (i.e. when `select(n)` is called). Because the `select(n)` + * instruction is not fully implemented yet (it doesn't actually execute + * host binding instruction code at the right time), this means that a + * styling apply function is still needed. + * + * This function is a mirror implementation of the `stylingApply()` + * instruction (found in `instructions/styling.ts`). + */ +export function stylingApply() { + const index = getSelectedIndex(); + const lView = getLView(); + const tNode = getTNode(index, lView); + const renderer = getRenderer(tNode, lView); + const native = getNativeFromLView(index, lView); + const directiveIndex = getActiveDirectiveStylingIndex(); + applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex); + + const sanitizer = getCurrentOrLViewSanitizer(lView); + applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex, sanitizer); + + setCurrentStyleSanitizer(null); +} + +/** + * Temporary function to bridge styling functionality between this new + * refactor (which is here inside of `styling_next/`) and the old + * implementation (which lives inside of `styling/`). + * + * The purpose of this function is to traverse through the LView data + * for a specific element index and return the native node. Because the + * current implementation relies on there being a styling context array, + * the code below will need to loop through these array values until it + * gets a native element node. + * + * Note that this code is temporary and will disappear once the new + * styling refactor lands in its entirety. + */ +function getNativeFromLView(index: number, viewData: LView): RElement { + let storageIndex = index + HEADER_OFFSET; + let slotValue: LContainer|LView|OldStylingContext|RElement = viewData[storageIndex]; + let wrapper: LContainer|LView|OldStylingContext = viewData; + while (Array.isArray(slotValue)) { + wrapper = slotValue; + slotValue = slotValue[HOST] as LView | OldStylingContext | RElement; + } + if (isOldStylingContext(wrapper)) { + return wrapper[OldStylingIndex.ElementPosition] as RElement; + } else { + return slotValue; + } +} + +function getRenderer(tNode: TNode, lView: LView) { + return tNode.type === TNodeType.Element ? lView[RENDERER] : null; +} + +/** + * Searches and assigns provided all static style/class entries (found in the `attrs` value) + * and registers them in their respective styling contexts. + */ +export function registerInitialStylingIntoContext( + tNode: TNode, attrs: TAttributes, startIndex: number) { + let classesContext !: TStylingContext; + let stylesContext !: TStylingContext; + let mode = -1; + for (let i = startIndex; i < attrs.length; i++) { + const attr = attrs[i]; + if (typeof attr == 'number') { + mode = attr; + } else if (mode == AttributeMarker.Classes) { + classesContext = classesContext || getClassesContext(tNode); + registerBinding(classesContext, -1, attr as string, true, false); + } else if (mode == AttributeMarker.Styles) { + stylesContext = stylesContext || getStylesContext(tNode); + registerBinding(stylesContext, -1, attr as string, attrs[++i] as string, false); + } + } +} + +/** + * Mirror implementation of the same function found in `instructions/styling.ts`. + */ +export function getActiveDirectiveStylingIndex(): number { + // whenever a directive's hostBindings function is called a uniqueId value + // is assigned. Normally this is enough to help distinguish one directive + // from another for the styling context, but there are situations where a + // sub-class directive could inherit and assign styling in concert with a + // parent directive. To help the styling code distinguish between a parent + // sub-classed directive the inheritance depth is taken into account as well. + return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth(); +} + +/** + * Temporary function that will update the max directive index value in + * both the classes and styles contexts present on the provided `tNode`. + * + * This code is only used because the `select(n)` code functionality is not + * yet 100% functional. The `select(n)` instruction cannot yet evaluate host + * bindings function code in sync with the associated template function code. + * For this reason the styling algorithm needs to track the last directive index + * value so that it knows exactly when to render styling to the element since + * `stylingApply()` is called multiple times per CD (`stylingApply` will be + * removed once `select(n)` is fixed). + */ +function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) { + updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex); + updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex); +} + +function getStylesContext(tNode: TNode): TStylingContext { + return getContext(tNode, false); +} + +function getClassesContext(tNode: TNode): TStylingContext { + return getContext(tNode, true); +} + +/** + * Returns/instantiates a styling context from/to a `tNode` instance. + */ +function getContext(tNode: TNode, isClassBased: boolean) { + let context = isClassBased ? tNode.newClasses : tNode.newStyles; + if (!context) { + context = allocTStylingContext(); + if (ngDevMode) { + attachStylingDebugObject(context); + } + if (isClassBased) { + tNode.newClasses = context; + } else { + tNode.newStyles = context; + } + } + return context; +} + +function resolveStylePropValue( + value: string | number | String | null, suffix: string | null | undefined) { + let resolvedValue: string|null = null; + if (value !== null) { + if (suffix) { + // when a suffix is applied then it will bypass + // sanitization entirely (b/c a new string is created) + resolvedValue = renderStringify(value) + suffix; + } else { + // sanitization happens by dealing with a String value + // this means that the string value will be passed through + // into the style rendering later (which is where the value + // will be sanitized before it is applied) + resolvedValue = value as any as string; + } + } + return resolvedValue; +} diff --git a/packages/core/src/render3/styling_next/interfaces.ts b/packages/core/src/render3/styling_next/interfaces.ts new file mode 100644 index 0000000000..3d99d9cbd5 --- /dev/null +++ b/packages/core/src/render3/styling_next/interfaces.ts @@ -0,0 +1,435 @@ +/** +* @license +* Copyright Google Inc. 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; +import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer'; +import {LView} from '../interfaces/view'; + +/** + * -------- + * + * This file contains the core interfaces for styling in Angular. + * + * To learn more about the algorithm see `TStylingContext`. + * + * -------- + */ + +/** + * A static-level representation of all style or class bindings/values + * associated with a `TNode`. + * + * The `TStylingContext` unites all template styling bindings (i.e. + * `[class]` and `[style]` bindings) as well as all host-level + * styling bindings (for components and directives) together into + * a single manifest. It is used each time there are one or more + * styling bindings present for an element. + * + * The styling context is stored on a `TNode` on and there are + * two instances of it: one for classes and another for styles. + * + * ```typescript + * tNode.styles = [ ... a context only for styles ... ]; + * tNode.classes = [ ... a context only for classes ... ]; + * ``` + * + * Due to the fact the the `TStylingContext` is stored on a `TNode` + * this means that all data within the context is static. Instead of + * storing actual styling binding values, the lView binding index values + * are stored within the context. (static nature means it is more compact.) + + * + * ```typescript + * //
// lView binding index = 22 + * tNode.stylesContext = [ + * 0, // the context config value + * + * 0b001, // guard mask for width + * 2, // total entries for width + * 'width', // the property name + * 21, // the binding location for the "x" binding in the lView + * null, + * + * 0b010, // guard mask for height + * 2, // total entries for height + * 'height', // the property name + * 22, // the binding location for the "y" binding in the lView + * null, + * ]; + * + * tNode.classesContext = [ + * 0, // the context config value + * + * 0b001, // guard mask for active + * 2, // total entries for active + * 'active', // the property name + * 20, // the binding location for the "c" binding in the lView + * null, + * ]; + * ``` + * + * Entry value present in an entry (called a tuple) within the + * styling context is as follows: + * + * ```typescript + * context = [ + * CONFIG, // the styling context config value + * //... + * guardMask, + * totalEntries, + * propName, + * bindingIndices..., + * defaultValue + * ]; + * ``` + * + * Below is a breakdown of each value: + * + * - **guardMask**: + * A numeric value where each bit represents a binding index + * location. Each binding index location is assigned based on + * a local counter value that increments each time an instruction + * is called: + * + * ``` + *
// binding index = 22 (counter index = 1) + * ``` + * + * In the example code above, if the `width` value where to change + * then the first bit in the local bit mask value would be flipped + * (and the second bit for when `height`). + * + * If and when there are more than 32 binding sources in the context + * (more than 32 `[style/class]` bindings) then the bit masking will + * overflow and we are left with a situation where a `-1` value will + * represent the bit mask. Due to the way that JavaScript handles + * negative values, when the bit mask is `-1` then all bits within + * that value will be automatically flipped (this is a quick and + * efficient way to flip all bits on the mask when a special kind + * of caching scenario occurs or when there are more than 32 bindings). + * + * - **totalEntries**: + * Each property present in the contains various binding sources of + * where the styling data could come from. This includes template + * level bindings, directive/component host bindings as well as the + * default value (or static value) all writing to the same property. + * This value depicts how many binding source entries exist for the + * property. + * + * The reason why the totalEntries value is needed is because the + * styling context is dynamic in size and it's not possible + * for the flushing or update algorithms to know when and where + * a property starts and ends without it. + * + * - **propName**: + * The CSS property name or class name (e.g `width` or `active`). + * + * - **bindingIndices...**: + * A series of numeric binding values that reflect where in the + * lView to find the style/class values associated with the property. + * Each value is in order in terms of priority (templates are first, + * then directives and then components). When the context is flushed + * and the style/class values are applied to the element (this happens + * inside of the `stylingApply` instruction) then the flushing code + * will keep checking each binding index against the associated lView + * to find the first style/class value that is non-null. + * + * - **defaultValue**: + * This is the default that will always be applied to the element if + * and when all other binding sources return a result that is null. + * Usually this value is null but it can also be a static value that + * is intercepted when the tNode is first constructured (e.g. + * `
` has a default value of `200px` for + * the `width` property). + * + * Each time a new binding is encountered it is registered into the + * context. The context then is continually updated until the first + * styling apply call has been called (this is triggered by the + * `stylingApply()` instruction for the active element). + * + * # How Styles/Classes are Rendered + * Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`, + * etc...) is executed, the associated `lView` for the view is updated + * at the current binding location. Also, when this happens, a local + * counter value is incremented. If the binding value has changed then + * a local `bitMask` variable is updated with the specific bit based + * on the counter value. + * + * Below is a lightweight example of what happens when a single style + * property is updated (i.e. `
`): + * + * ```typescript + * function updateStyleProp(prop: string, value: string) { + * const lView = getLView(); + * const bindingIndex = BINDING_INDEX++; + * const indexForStyle = localStylesCounter++; + * if (lView[bindingIndex] !== value) { + * lView[bindingIndex] = value; + * localBitMaskForStyles |= 1 << indexForStyle; + * } + * } + * ``` + * + * ## The Apply Algorithm + * As explained above, each time a binding updates its value, the resulting + * value is stored in the `lView` array. These styling values have yet to + * be flushed to the element. + * + * Once all the styling instructions have been evaluated, then the styling + * context(s) are flushed to the element. When this happens, the context will + * be iterated over (property by property) and each binding source will be + * examined and the first non-null value will be applied to the element. + * + * Let's say that we the following template code: + * + * ```html + *
+ * ``` + * + * There are two styling bindings in the code above and they both write + * to the `width` property. When styling is flushed on the element, the + * algorithm will try and figure out which one of these values to write + * to the element. + * + * In order to figure out which value to apply, the following + * binding prioritization is adhered to: + * + * 1. First template-level styling bindings are applied (if present). + * This includes things like `[style.width]` and `[class.active]`. + * + * 2. Second are styling-level host bindings present in directives. + * (if there are sub/super directives present then the sub directives + * are applied first). + * + * 3. Third are styling-level host bindings present in components. + * (if there are sub/super components present then the sub directives + * are applied first). + * + * This means that in the code above the styling binding present in the + * template is applied first and, only if its falsy, then the directive + * styling binding for width will be applied. + * + * ### What about map-based styling bindings? + * Map-based styling bindings are activated when there are one or more + * `[style]` and/or `[class]` bindings present on an element. When this + * code is activated, the apply algorithm will iterate over each map + * entry and apply each styling value to the element with the same + * prioritization rules as above. + * + * For the algorithm to apply styling values efficiently, the + * styling map entries must be applied in sync (property by property) + * with prop-based bindings. (The map-based algorithm is described + * more inside of the `render3/styling_next/map_based_bindings.ts` file.) + * + * ## Sanitization + * Sanitization is used to prevent invalid style values from being applied to + * the element. + * + * It is enabled in two cases: + * + * 1. The `styleSanitizer(sanitizerFn)` instruction was called (just before any other + * styling instructions are run). + * + * 2. The component/directive `LView` instance has a sanitizer object attached to it + * (this happens when `renderComponent` is executed with a `sanitizer` value or + * if the ngModule contains a sanitizer provider attached to it). + * + * If and when sanitization is active then all property/value entries will be evaluated + * through the active sanitizer before they are applied to the element (or the styling + * debug handler). + * + * If a `Sanitizer` object is used (via the `LView[SANITIZER]` value) then that object + * will be used for every property. + * + * If a `StyleSanitizerFn` function is used (via the `styleSanitizer`) then it will be + * called in two ways: + * + * 1. property validation mode: this will be called early to mark whether a property + * should be sanitized or not at during the flushing stage. + * + * 2. value sanitization mode: this will be called during the flushing stage and will + * run the sanitizer function against the value before applying it to the element. + * + * If sanitization returns an empty value then that empty value will be applied + * to the element. + */ +export interface TStylingContext extends Array { + /** Configuration data for the context */ + [TStylingContextIndex.ConfigPosition]: TStylingConfigFlags; + + /** Temporary value used to track directive index entries until + the old styling code is fully removed. The reason why this + is required is to figure out which directive is last and, + when encountered, trigger a styling flush to happen */ + [TStylingContextIndex.MaxDirectiveIndexPosition]: number; + + /** The bit guard value for all map-based bindings on an element */ + [TStylingContextIndex.MapBindingsBitGuardPosition]: number; + + /** The total amount of map-based bindings present on an element */ + [TStylingContextIndex.MapBindingsValuesCountPosition]: number; + + /** The prop value for map-based bindings (there actually isn't a + * value at all, but this is just used in the context to avoid + * having any special code to update the binding information for + * map-based entries). */ + [TStylingContextIndex.MapBindingsPropPosition]: string; +} + +/** + * A series of flags used to configure the config value present within a + * `TStylingContext` value. + */ +export const enum TStylingConfigFlags { + /** + * The initial state of the styling context config + */ + Initial = 0b0, + + /** + * A flag which marks the context as being locked. + * + * The styling context is constructed across an element template + * function as well as any associated hostBindings functions. When + * this occurs, the context itself is open to mutation and only once + * it has been flushed once then it will be locked for good (no extra + * bindings can be added to it). + */ + Locked = 0b1, +} + +/** + * An index of position and offset values used to natigate the `TStylingContext`. + */ +export const enum TStylingContextIndex { + ConfigPosition = 0, + MaxDirectiveIndexPosition = 1, + + // index/offset values for map-based entries (i.e. `[style]` + // and `[class] bindings). + MapBindingsPosition = 2, + MapBindingsBitGuardPosition = 2, + MapBindingsValuesCountPosition = 3, + MapBindingsPropPosition = 4, + MapBindingsBindingsStartPosition = 5, + + // each tuple entry in the context + // (mask, count, prop, ...bindings||default-value) + ConfigAndGuardOffset = 0, + ValuesCountOffset = 1, + PropOffset = 2, + BindingsStartOffset = 3, +} + +/** + * A series of flags used for each property entry within the `TStylingContext`. + */ +export const enum TStylingContextPropConfigFlags { + Default = 0b0, + SanitizationRequired = 0b1, + TotalBits = 1, + Mask = 0b1, +} + +/** + * A function used to apply or remove styling from an element for a given property. + */ +export interface ApplyStylingFn { + (renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, + value: string|null, bindingIndex?: number|null): void; +} + +/** + * Runtime data type that is used to store binding data referenced from the `TStylingContext`. + * + * Because `LView` is just an array with data, there is no reason to + * special case `LView` everywhere in the styling algorithm. By allowing + * this data type to be an array that contains various scalar data types, + * an instance of `LView` doesn't need to be constructed for tests. + */ +export type LStylingData = LView | (string | number | boolean | null)[]; + +/** + * Array-based representation of a key/value array. + * + * The format of the array is "property", "value", "property2", + * "value2", etc... + * + * The first value in the array is reserved to store the instance + * of the key/value array that was used to populate the property/ + * value entries that take place in the remainder of the array. + */ +export interface LStylingMap extends Array<{}|string|number|null> { + [LStylingMapIndex.RawValuePosition]: {}|string|null; +} + +/** + * An index of position and offset points for any data stored within a `LStylingMap` instance. + */ +export const enum LStylingMapIndex { + /** The location of the raw key/value map instance used last to populate the array entries */ + RawValuePosition = 0, + + /** Where the values start in the array */ + ValuesStartPosition = 1, + + /** The size of each property/value entry */ + TupleSize = 2, + + /** The offset for the property entry in the tuple */ + PropOffset = 0, + + /** The offset for the value entry in the tuple */ + ValueOffset = 1, +} + +/** + * Used to apply/traverse across all map-based styling entries up to the provided `targetProp` + * value. + * + * When called, each of the map-based `LStylingMap` entries (which are stored in + * the provided `LStylingData` array) will be iterated over. Depending on the provided + * `mode` value, each prop/value entry may be applied or skipped over. + * + * If `targetProp` value is provided the iteration code will stop once it reaches + * the property (if found). Otherwise if the target property is not encountered then + * it will stop once it reaches the next value that appears alphabetically after it. + * + * If a `defaultValue` is provided then it will be applied to the element only if the + * `targetProp` property value is encountered and the value associated with the target + * property is `null`. The reason why the `defaultValue` is needed is to avoid having the + * algorithm apply a `null` value and then apply a default value afterwards (this would + * end up being two style property writes). + * + * @returns whether or not the target property was reached and its value was + * applied to the element. + */ +export interface SyncStylingMapsFn { + (context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement, + data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn|null, + mode: StylingMapsSyncMode, targetProp?: string|null, defaultValue?: string|null): boolean; +} + +/** + * Used to direct how map-based values are applied/traversed when styling is flushed. + */ +export const enum StylingMapsSyncMode { + /** Only traverse values (no prop/value styling entries get applied) */ + TraverseValues = 0b000, + + /** Apply every prop/value styling entry to the element */ + ApplyAllValues = 0b001, + + /** Only apply the target prop/value entry */ + ApplyTargetProp = 0b010, + + /** Skip applying the target prop/value entry */ + SkipTargetProp = 0b100, +} diff --git a/packages/core/src/render3/styling_next/map_based_bindings.ts b/packages/core/src/render3/styling_next/map_based_bindings.ts new file mode 100644 index 0000000000..d24f67eec4 --- /dev/null +++ b/packages/core/src/render3/styling_next/map_based_bindings.ts @@ -0,0 +1,381 @@ +/** +* @license +* Copyright Google Inc. 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 {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; +import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer'; + +import {setStylingMapsSyncFn} from './bindings'; +import {ApplyStylingFn, LStylingData, LStylingMap, LStylingMapIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingContext, TStylingContextIndex} from './interfaces'; +import {getBindingValue, getValuesCount, isStylingValueDefined} from './util'; + + +/** + * -------- + * + * This file contains the algorithm logic for applying map-based bindings + * such as `[style]` and `[class]`. + * + * -------- + */ + +/** + * Used to apply styling values presently within any map-based bindings on an element. + * + * Angular supports map-based styling bindings which can be applied via the + * `[style]` and `[class]` bindings which can be placed on any HTML element. + * These bindings can work independently, together or alongside prop-based + * styling bindings (e.g. `
`). + * + * If a map-based styling binding is detected by the compiler, the following + * AOT code is produced: + * + * ```typescript + * styleMap(ctx.styles); // styles = {key:value} + * classMap(ctx.classes); // classes = {key:value}|string + * ``` + * + * If and when either of the instructions above are evaluated, then the code + * present in this file is included into the bundle. The mechanism used, to + * activate support for map-based bindings at runtime is possible via the + * `activeStylingMapFeature` function (which is also present in this file). + * + * # The Algorithm + * Whenever a map-based binding updates (which is when the identity of the + * map-value changes) then the map is iterated over and a `LStylingMap` array + * is produced. The `LStylingMap` instance is stored in the binding location + * where the `BINDING_INDEX` is situated when the `styleMap()` or `classMap()` + * instruction were called. Once the binding changes, then the internal `bitMask` + * value is marked as dirty. + * + * Styling values are applied once CD exits the element (which happens when + * the `select(n)` instruction is called or the template function exits). When + * this occurs, all prop-based bindings are applied. If a map-based binding is + * present then a special flushing function (called a sync function) is made + * available and it will be called each time a styling property is flushed. + * + * The flushing algorithm is designed to apply styling for a property (which is + * a CSS property or a className value) one by one. If map-based bindings + * are present, then the flushing algorithm will keep calling the maps styling + * sync function each time a property is visited. This way, the flushing + * behavior of map-based bindings will always be at the same property level + * as the current prop-based property being iterated over (because everything + * is alphabetically sorted). + * + * Let's imagine we have the following HTML template code: + * + * ```html + *
...
+ * ``` + * + * When CD occurs, both the `[style]` and `[style.width]` bindings + * are evaluated. Then when the styles are flushed on screen, the + * following operations happen: + * + * 1. `[style.width]` is attempted to be written to the element. + * + * 2. Once that happens, the algorithm instructs the map-based + * entries (`[style]` in this case) to "catch up" and apply + * all values up to the `width` value. When this happens the + * `height` value is applied to the element (since it is + * alphabetically situated before the `width` property). + * + * 3. Since there are no more prop-based entries anymore, the + * loop exits and then, just before the flushing ends, it + * instructs all map-based bindings to "finish up" applying + * their values. + * + * 4. The only remaining value within the map-based entries is + * the `z-index` value (`width` got skipped because it was + * successfully applied via the prop-based `[style.width]` + * binding). Since all map-based entries are told to "finish up", + * the `z-index` value is iterated over and it is then applied + * to the element. + * + * The most important thing to take note of here is that prop-based + * bindings are evaluated in order alongside map-based bindings. + * This allows all styling across an element to be applied in O(n) + * time (a similar algorithm is that of the array merge algorithm + * in merge sort). + */ +export const syncStylingMap: SyncStylingMapsFn = + (context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, + data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null, + mode: StylingMapsSyncMode, targetProp?: string | null, + defaultValue?: string | null): boolean => { + let targetPropValueWasApplied = false; + + // once the map-based styling code is activate it is never deactivated. For this reason a + // check to see if the current styling context has any map based bindings is required. + const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); + if (totalMaps) { + let runTheSyncAlgorithm = true; + const loopUntilEnd = !targetProp; + + // If the code is told to finish up (run until the end), but the mode + // hasn't been flagged to apply values (it only traverses values) then + // there is no point in iterating over the array because nothing will + // be applied to the element. + if (loopUntilEnd && (mode & ~StylingMapsSyncMode.ApplyAllValues)) { + runTheSyncAlgorithm = false; + targetPropValueWasApplied = true; + } + + if (runTheSyncAlgorithm) { + targetPropValueWasApplied = innerSyncStylingMap( + context, renderer, element, data, applyStylingFn, sanitizer, mode, targetProp || null, + 0, defaultValue || null); + } + + if (loopUntilEnd) { + resetSyncCursors(); + } + } + + return targetPropValueWasApplied; + }; + +/** + * Recursive function designed to apply map-based styling to an element one map at a time. + * + * This function is designed to be called from the `syncStylingMap` function and will + * apply map-based styling data one map at a time to the provided `element`. + * + * This function is recursive and it will call itself if a follow-up map value is to be + * processed. To learn more about how the algorithm works, see `syncStylingMap`. + */ +function innerSyncStylingMap( + context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement, + data: LStylingData, applyStylingFn: ApplyStylingFn, sanitizer: StyleSanitizeFn | null, + mode: StylingMapsSyncMode, targetProp: string | null, currentMapIndex: number, + defaultValue: string | null): boolean { + let targetPropValueWasApplied = false; + + const totalMaps = getValuesCount(context, TStylingContextIndex.MapBindingsPosition); + if (currentMapIndex < totalMaps) { + const bindingIndex = getBindingValue( + context, TStylingContextIndex.MapBindingsPosition, currentMapIndex) as number; + const lStylingMap = data[bindingIndex] as LStylingMap; + + let cursor = getCurrentSyncCursor(currentMapIndex); + while (cursor < lStylingMap.length) { + const prop = getMapProp(lStylingMap, cursor); + const iteratedTooFar = targetProp && prop > targetProp; + const isTargetPropMatched = !iteratedTooFar && prop === targetProp; + const value = getMapValue(lStylingMap, cursor); + const valueIsDefined = isStylingValueDefined(value); + + // the recursive code is designed to keep applying until + // it reaches or goes past the target prop. If and when + // this happens then it will stop processing values, but + // all other map values must also catch up to the same + // point. This is why a recursive call is still issued + // even if the code has iterated too far. + const innerMode = + iteratedTooFar ? mode : resolveInnerMapMode(mode, valueIsDefined, isTargetPropMatched); + const innerProp = iteratedTooFar ? targetProp : prop; + let valueApplied = innerSyncStylingMap( + context, renderer, element, data, applyStylingFn, sanitizer, innerMode, innerProp, + currentMapIndex + 1, defaultValue); + + if (iteratedTooFar) { + break; + } + + if (!valueApplied && isValueAllowedToBeApplied(mode, isTargetPropMatched)) { + const useDefault = isTargetPropMatched && !valueIsDefined; + const valueToApply = useDefault ? defaultValue : value; + const bindingIndexToApply = useDefault ? bindingIndex : null; + const finalValue = sanitizer ? + sanitizer(prop, valueToApply, StyleSanitizeMode.ValidateAndSanitize) : + valueToApply; + applyStylingFn(renderer, element, prop, finalValue, bindingIndexToApply); + valueApplied = true; + } + + targetPropValueWasApplied = valueApplied && isTargetPropMatched; + cursor += LStylingMapIndex.TupleSize; + } + setCurrentSyncCursor(currentMapIndex, cursor); + } + + return targetPropValueWasApplied; +} + + +/** + * Enables support for map-based styling bindings (e.g. `[style]` and `[class]` bindings). + */ +export function activeStylingMapFeature() { + setStylingMapsSyncFn(syncStylingMap); +} + +/** + * Used to determine the mode for the inner recursive call. + * + * If an inner map is iterated on then this is done so for one + * of two reasons: + * + * - The target property was detected and the inner map + * must now "catch up" (pointer-wise) up to where the current + * map's cursor is situated. + * + * - The target property was not detected in the current map + * and must be found in an inner map. This can only be allowed + * if the current map iteration is not set to skip the target + * property. + */ +function resolveInnerMapMode( + currentMode: number, valueIsDefined: boolean, isExactMatch: boolean): number { + let innerMode = currentMode; + if (!valueIsDefined && isExactMatch && !(currentMode & StylingMapsSyncMode.SkipTargetProp)) { + // case 1: set the mode to apply the targeted prop value if it + // ends up being encountered in another map value + innerMode |= StylingMapsSyncMode.ApplyTargetProp; + innerMode &= ~StylingMapsSyncMode.SkipTargetProp; + } else { + // case 2: set the mode to skip the targeted prop value if it + // ends up being encountered in another map value + innerMode |= StylingMapsSyncMode.SkipTargetProp; + innerMode &= ~StylingMapsSyncMode.ApplyTargetProp; + } + return innerMode; +} + +/** + * Decides whether or not a prop/value entry will be applied to an element. + * + * To determine whether or not a value is to be applied, + * the following procedure is evaluated: + * + * First check to see the current `mode` status: + * 1. If the mode value permits all props to be applied then allow. + * - But do not allow if the current prop is set to be skipped. + * 2. Otherwise if the current prop is permitted then allow. + */ +function isValueAllowedToBeApplied(mode: number, isTargetPropMatched: boolean) { + let doApplyValue = (mode & StylingMapsSyncMode.ApplyAllValues) > 0; + if (!doApplyValue) { + if (mode & StylingMapsSyncMode.ApplyTargetProp) { + doApplyValue = isTargetPropMatched; + } + } else if ((mode & StylingMapsSyncMode.SkipTargetProp) && isTargetPropMatched) { + doApplyValue = false; + } + return doApplyValue; +} + +/** + * Used to keep track of concurrent cursor values for multiple map-based styling bindings present on + * an element. + */ +const MAP_CURSORS: number[] = []; + +/** + * Used to reset the state of each cursor value being used to iterate over map-based styling + * bindings. + */ +function resetSyncCursors() { + for (let i = 0; i < MAP_CURSORS.length; i++) { + MAP_CURSORS[i] = LStylingMapIndex.ValuesStartPosition; + } +} + +/** + * Returns an active cursor value at a given mapIndex location. + */ +function getCurrentSyncCursor(mapIndex: number) { + if (mapIndex >= MAP_CURSORS.length) { + MAP_CURSORS.push(LStylingMapIndex.ValuesStartPosition); + } + return MAP_CURSORS[mapIndex]; +} + +/** + * Sets a cursor value at a given mapIndex location. + */ +function setCurrentSyncCursor(mapIndex: number, indexValue: number) { + MAP_CURSORS[mapIndex] = indexValue; +} + +/** + * Used to convert a {key:value} map into a `LStylingMap` array. + * + * This function will either generate a new `LStylingMap` instance + * or it will patch the provided `newValues` map value into an + * existing `LStylingMap` value (this only happens if `bindingValue` + * is an instance of `LStylingMap`). + * + * If a new key/value map is provided with an old `LStylingMap` + * value then all properties will be overwritten with their new + * values or with `null`. This means that the array will never + * shrink in size (but it will also not be created and thrown + * away whenever the {key:value} map entries change). + */ +export function normalizeIntoStylingMap( + bindingValue: null | LStylingMap, + newValues: {[key: string]: any} | string | null | undefined): LStylingMap { + const lStylingMap: LStylingMap = Array.isArray(bindingValue) ? bindingValue : [null]; + lStylingMap[LStylingMapIndex.RawValuePosition] = newValues || null; + + // because the new values may not include all the properties + // that the old ones had, all values are set to `null` before + // the new values are applied. This way, when flushed, the + // styling algorithm knows exactly what style/class values + // to remove from the element (since they are `null`). + for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length; + j += LStylingMapIndex.TupleSize) { + setMapValue(lStylingMap, j, null); + } + + let props: string[]|null = null; + let map: {[key: string]: any}|undefined|null; + let allValuesTrue = false; + if (typeof newValues === 'string') { // [class] bindings allow string values + if (newValues.length) { + props = newValues.split(/\s+/); + allValuesTrue = true; + } + } else { + props = newValues ? Object.keys(newValues) : null; + map = newValues; + } + + if (props) { + outer: for (let i = 0; i < props.length; i++) { + const prop = props[i] as string; + const value = allValuesTrue ? true : map ![prop]; + for (let j = LStylingMapIndex.ValuesStartPosition; j < lStylingMap.length; + j += LStylingMapIndex.TupleSize) { + const propAtIndex = getMapProp(lStylingMap, j); + if (prop <= propAtIndex) { + if (propAtIndex === prop) { + setMapValue(lStylingMap, j, value); + } else { + lStylingMap.splice(j, 0, prop, value); + } + continue outer; + } + } + lStylingMap.push(prop, value); + } + } + + return lStylingMap; +} + +export function getMapProp(map: LStylingMap, index: number): string { + return map[index + LStylingMapIndex.PropOffset] as string; +} + +export function setMapValue(map: LStylingMap, index: number, value: string | null): void { + map[index + LStylingMapIndex.ValueOffset] = value; +} + +export function getMapValue(map: LStylingMap, index: number): string|null { + return map[index + LStylingMapIndex.ValueOffset] as string | null; +} diff --git a/packages/core/src/render3/styling_next/state.ts b/packages/core/src/render3/styling_next/state.ts new file mode 100644 index 0000000000..50e42c6807 --- /dev/null +++ b/packages/core/src/render3/styling_next/state.ts @@ -0,0 +1,61 @@ +/** +* @license +* Copyright Google Inc. 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 {Sanitizer} from '../../sanitization/security'; +import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; + +/** + * -------- + * + * This file contains temporary code to incorporate the new styling refactor + * code to work alongside the existing instruction set. + * + * This file will be removed once `select(n)` is fully functional (once + * it is able to evaluate host bindings in sync element-by-element + * with template code). + * + * -------- + */ + +/** + * A temporary enum of states that inform the core whether or not + * to defer all styling instruction calls to the old or new + * styling implementation. + */ +export const enum RuntimeStylingMode { + UseOld = 0, + UseBothOldAndNew = 1, + UseNew = 2, +} + +let _stylingMode = 0; + +/** + * Temporary function used to inform the existing styling algorithm + * code to delegate all styling instruction calls to the new refactored + * styling code. + */ +export function runtimeSetStylingMode(mode: RuntimeStylingMode) { + _stylingMode = mode; +} + +export function runtimeIsNewStylingInUse() { + return _stylingMode > RuntimeStylingMode.UseOld; +} + +export function runtimeAllowOldStyling() { + return _stylingMode < RuntimeStylingMode.UseNew; +} + +let _currentSanitizer: Sanitizer|StyleSanitizeFn|null; +export function setCurrentStyleSanitizer(sanitizer: Sanitizer | StyleSanitizeFn | null) { + _currentSanitizer = sanitizer; +} + +export function getCurrentStyleSanitizer() { + return _currentSanitizer; +} diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts new file mode 100644 index 0000000000..2f78274321 --- /dev/null +++ b/packages/core/src/render3/styling_next/styling_debug.ts @@ -0,0 +1,223 @@ +/** +* @license +* Copyright Google Inc. 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 {Sanitizer} from '../../sanitization/security'; +import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; +import {RElement} from '../interfaces/renderer'; +import {LView, SANITIZER} from '../interfaces/view'; +import {attachDebugObject} from '../util/debug_utils'; + +import {applyStyling} from './bindings'; +import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex} from './interfaces'; +import {activeStylingMapFeature} from './map_based_bindings'; +import {getCurrentStyleSanitizer} from './state'; +import {getCurrentOrLViewSanitizer, getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked, isMapBased, isSanitizationRequired} from './util'; + + + +/** + * -------- + * + * This file contains the core debug functionality for styling in Angular. + * + * To learn more about the algorithm see `TStylingContext`. + * + * -------- + */ + + +/** + * A debug/testing-oriented summary of a styling entry. + * + * A value such as this is generated as an artifact of the `DebugStyling` + * summary. + */ +export interface LStylingSummary { + /** The style/class property that the summary is attached to */ + prop: string; + + /** The last applied value for the style/class property */ + value: string|boolean|null; + + /** The binding index of the last applied style/class property */ + bindingIndex: number|null; +} + +/** + * A debug/testing-oriented summary of all styling entries for a `DebugNode` instance. + */ +export interface DebugStyling { + /** The associated TStylingContext instance */ + context: TStylingContext; + + /** + * A summarization of each style/class property + * present in the context. + */ + summary: {[key: string]: LStylingSummary}; + + /** + * A key/value map of all styling properties and their + * runtime values. + */ + values: {[key: string]: string | number | null | boolean}; + + /** + * Overrides the sanitizer used to process styles. + */ + overrideSanitizer(sanitizer: StyleSanitizeFn|null): void; +} + +/** + * A debug/testing-oriented summary of all styling entries within a `TStylingContext`. + */ +export interface TStylingTupleSummary { + /** The property (style or class property) that this tuple represents */ + prop: string; + + /** The total amount of styling entries apart of this tuple */ + valuesCount: number; + + /** + * The bit guard mask that is used to compare and protect against + * styling changes when and styling bindings update + */ + guardMask: number; + + /** + * Whether or not the entry requires sanitization + */ + sanitizationRequired: boolean; + + /** + * The default value that will be applied if any bindings are falsy. + */ + defaultValue: string|boolean|null; + + /** + * All bindingIndex sources that have been registered for this style. + */ + sources: (number|null|string)[]; +} + +/** + * Instantiates and attaches an instance of `TStylingContextDebug` to the provided context. + */ +export function attachStylingDebugObject(context: TStylingContext) { + const debug = new TStylingContextDebug(context); + attachDebugObject(context, debug); + return debug; +} + +/** + * A human-readable debug summary of the styling data present within `TStylingContext`. + * + * This class is designed to be used within testing code or when an + * application has `ngDevMode` activated. + */ +class TStylingContextDebug { + constructor(public readonly context: TStylingContext) {} + + get isLocked() { return isContextLocked(this.context); } + + /** + * Returns a detailed summary of each styling entry in the context. + * + * See `TStylingTupleSummary`. + */ + get entries(): {[prop: string]: TStylingTupleSummary} { + const context = this.context; + const entries: {[prop: string]: TStylingTupleSummary} = {}; + const start = TStylingContextIndex.MapBindingsPosition; + let i = start; + while (i < context.length) { + const valuesCount = getValuesCount(context, i); + // the context may contain placeholder values which are populated ahead of time, + // but contain no actual binding values. In this situation there is no point in + // classifying this as an "entry" since no real data is stored here yet. + if (valuesCount) { + const prop = getProp(context, i); + const guardMask = getGuardMask(context, i); + const defaultValue = getDefaultValue(context, i); + const sanitizationRequired = isSanitizationRequired(context, i); + const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset; + + const sources: (number | string | null)[] = []; + for (let j = 0; j < valuesCount; j++) { + sources.push(context[bindingsStartPosition + j] as number | string | null); + } + + entries[prop] = {prop, guardMask, sanitizationRequired, valuesCount, defaultValue, sources}; + } + + i += TStylingContextIndex.BindingsStartOffset + valuesCount; + } + return entries; + } +} + +/** + * A human-readable debug summary of the styling data present for a `DebugNode` instance. + * + * This class is designed to be used within testing code or when an + * application has `ngDevMode` activated. + */ +export class NodeStylingDebug implements DebugStyling { + private _sanitizer: StyleSanitizeFn|null = null; + + constructor( + public context: TStylingContext, private _data: LStylingData, + private _isClassBased?: boolean) {} + + /** + * Overrides the sanitizer used to process styles. + */ + overrideSanitizer(sanitizer: StyleSanitizeFn|null) { this._sanitizer = sanitizer; } + + /** + * Returns a detailed summary of each styling entry in the context and + * what their runtime representation is. + * + * See `LStylingSummary`. + */ + get summary(): {[key: string]: LStylingSummary} { + const entries: {[key: string]: LStylingSummary} = {}; + this._mapValues((prop: string, value: any, bindingIndex: number | null) => { + entries[prop] = {prop, value, bindingIndex}; + }); + return entries; + } + + /** + * Returns a key/value map of all the styles/classes that were last applied to the element. + */ + get values(): {[key: string]: any} { + const entries: {[key: string]: any} = {}; + this._mapValues((prop: string, value: any) => { entries[prop] = value; }); + return entries; + } + + private _mapValues(fn: (prop: string, value: any, bindingIndex: number|null) => any) { + // there is no need to store/track an element instance. The + // element is only used when the styling algorithm attempts to + // style the value (and we mock out the stylingApplyFn anyway). + const mockElement = {} as any; + const hasMaps = getValuesCount(this.context, TStylingContextIndex.MapBindingsPosition) > 0; + if (hasMaps) { + activeStylingMapFeature(); + } + + const mapFn: ApplyStylingFn = + (renderer: any, element: RElement, prop: string, value: any, bindingIndex: number) => { + fn(prop, value, bindingIndex || null); + }; + + const sanitizer = this._isClassBased ? null : (this._sanitizer || + getCurrentOrLViewSanitizer(this._data as LView)); + applyStyling(this.context, null, mockElement, this._data, true, mapFn, sanitizer); + } +} diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts new file mode 100644 index 0000000000..494738403a --- /dev/null +++ b/packages/core/src/render3/styling_next/util.ts @@ -0,0 +1,176 @@ +/** +* @license +* Copyright Google Inc. 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 {Sanitizer, SecurityContext} from '../../sanitization/security'; +import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer'; +import {StylingContext} from '../interfaces/styling'; +import {LView, SANITIZER} from '../interfaces/view'; +import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings'; + +import {LStylingMap, LStylingMapIndex, TStylingConfigFlags, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from './interfaces'; +import {getCurrentStyleSanitizer, setCurrentStyleSanitizer} from './state'; + +const MAP_BASED_ENTRY_PROP_NAME = '--MAP--'; + +/** + * Creates a new instance of the `TStylingContext`. + * + * This function will also pre-fill the context with data + * for map-based bindings. + */ +export function allocTStylingContext(): TStylingContext { + // because map-based bindings deal with a dynamic set of values, there + // is no way to know ahead of time whether or not sanitization is required. + // For this reason the configuration will always mark sanitization as active + // (this means that when map-based values are applied then sanitization will + // be checked against each property). + const mapBasedConfig = TStylingContextPropConfigFlags.SanitizationRequired; + return [TStylingConfigFlags.Initial, 0, mapBasedConfig, 0, MAP_BASED_ENTRY_PROP_NAME]; +} + +/** + * Temporary function that allows for a string-based property name to be + * obtained from an index-based property identifier. + * + * This function will be removed once the new styling refactor code (which + * lives inside of `render3/styling_next/`) replaces the existing styling + * implementation. + */ +export function getBindingNameFromIndex( + stylingContext: StylingContext, offset: number, directiveIndex: number, isClassBased: boolean) { + const singleIndex = + getOldSinglePropIndexValue(stylingContext, directiveIndex, offset, isClassBased); + return getOldProp(stylingContext, singleIndex); +} + +export function updateContextDirectiveIndex(context: TStylingContext, index: number) { + context[TStylingContextIndex.MaxDirectiveIndexPosition] = index; +} + +function getConfig(context: TStylingContext) { + return context[TStylingContextIndex.ConfigPosition]; +} + +export function setConfig(context: TStylingContext, value: number) { + context[TStylingContextIndex.ConfigPosition] = value; +} + +export function getProp(context: TStylingContext, index: number) { + return context[index + TStylingContextIndex.PropOffset] as string; +} + +function getPropConfig(context: TStylingContext, index: number): number { + return (context[index + TStylingContextIndex.ConfigAndGuardOffset] as number) & + TStylingContextPropConfigFlags.Mask; +} + +export function isSanitizationRequired(context: TStylingContext, index: number) { + return (getPropConfig(context, index) & TStylingContextPropConfigFlags.SanitizationRequired) > 0; +} + +export function getGuardMask(context: TStylingContext, index: number) { + const configGuardValue = context[index + TStylingContextIndex.ConfigAndGuardOffset] as number; + return configGuardValue >> TStylingContextPropConfigFlags.TotalBits; +} + +export function setGuardMask(context: TStylingContext, index: number, maskValue: number) { + const config = getPropConfig(context, index); + const guardMask = maskValue << TStylingContextPropConfigFlags.TotalBits; + context[index + TStylingContextIndex.ConfigAndGuardOffset] = config | guardMask; +} + +export function getValuesCount(context: TStylingContext, index: number) { + return context[index + TStylingContextIndex.ValuesCountOffset] as number; +} + +export function getBindingValue(context: TStylingContext, index: number, offset: number) { + return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string; +} + +export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null { + const valuesCount = getValuesCount(context, index); + return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string | + boolean | null; +} + +/** + * Temporary function which determines whether or not a context is + * allowed to be flushed based on the provided directive index. + */ +export function allowStylingFlush(context: TStylingContext, index: number) { + return index === context[TStylingContextIndex.MaxDirectiveIndexPosition]; +} + +export function lockContext(context: TStylingContext) { + setConfig(context, getConfig(context) | TStylingConfigFlags.Locked); +} + +export function isContextLocked(context: TStylingContext): boolean { + return (getConfig(context) & TStylingConfigFlags.Locked) > 0; +} + +export function getPropValuesStartPosition(context: TStylingContext) { + return TStylingContextIndex.MapBindingsBindingsStartPosition + + context[TStylingContextIndex.MapBindingsValuesCountPosition]; +} + +export function isMapBased(prop: string) { + return prop === MAP_BASED_ENTRY_PROP_NAME; +} + +export function hasValueChanged( + a: LStylingMap | number | String | string | null | boolean | undefined | {}, + b: LStylingMap | number | String | string | null | boolean | undefined | {}): boolean { + const compareValueA = Array.isArray(a) ? a[LStylingMapIndex.RawValuePosition] : a; + const compareValueB = Array.isArray(b) ? b[LStylingMapIndex.RawValuePosition] : b; + return compareValueA !== compareValueB; +} + +/** + * Determines whether the provided styling value is truthy or falsy. + */ +export function isStylingValueDefined(value: any) { + // the reason why null is compared against is because + // a CSS class value that is set to `false` must be + // respected (otherwise it would be treated as falsy). + // Empty string values are because developers usually + // set a value to an empty string to remove it. + return value != null && value !== ''; +} + +/** + * Returns the current style sanitizer function for the given view. + * + * The default style sanitizer (which lives inside of `LView`) will + * be returned depending on whether the `styleSanitizer` instruction + * was called or not prior to any styling instructions running. + */ +export function getCurrentOrLViewSanitizer(lView: LView): StyleSanitizeFn|null { + const sanitizer: StyleSanitizeFn|null = (getCurrentStyleSanitizer() || lView[SANITIZER]) as any; + if (sanitizer && typeof sanitizer !== 'function') { + setCurrentStyleSanitizer(sanitizer); + return sanitizeUsingSanitizerObject; + } + return sanitizer; +} + +/** + * Style sanitization function that internally uses a `Sanitizer` instance to handle style + * sanitization. + */ +const sanitizeUsingSanitizerObject: StyleSanitizeFn = + (prop: string, value: string, mode: StyleSanitizeMode) => { + const sanitizer = getCurrentStyleSanitizer() as Sanitizer; + if (sanitizer) { + if (mode & StyleSanitizeMode.SanitizeOnly) { + return sanitizer.sanitize(SecurityContext.STYLE, value); + } else { + return true; + } + } + return value; + }; diff --git a/packages/core/src/render3/util/attrs_utils.ts b/packages/core/src/render3/util/attrs_utils.ts index 445404948a..c64e2df6ce 100644 --- a/packages/core/src/render3/util/attrs_utils.ts +++ b/packages/core/src/render3/util/attrs_utils.ts @@ -109,8 +109,9 @@ export function attrsStylingIndexOf(attrs: TAttributes, startIndex: number): num * attribute values in a `TAttributes` array are only the names of attributes, * and not name-value pairs. * @param marker The attribute marker to test. - * @returns true if the marker is a "name-only" marker (e.g. `Bindings` or `Template`). + * @returns true if the marker is a "name-only" marker (e.g. `Bindings`, `Template` or `I18n`). */ export function isNameOnlyAttributeMarker(marker: string | AttributeMarker | CssSelector) { - return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template; + return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template || + marker === AttributeMarker.I18n; } diff --git a/packages/core/src/render3/util/debug_utils.ts b/packages/core/src/render3/util/debug_utils.ts new file mode 100644 index 0000000000..3c30200ed1 --- /dev/null +++ b/packages/core/src/render3/util/debug_utils.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. 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 + */ +export function attachDebugObject(obj: any, debug: any) { + Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); +} diff --git a/packages/core/src/render3/util/misc_utils.ts b/packages/core/src/render3/util/misc_utils.ts index a72006f1ed..cf45c6f6c4 100644 --- a/packages/core/src/render3/util/misc_utils.ts +++ b/packages/core/src/render3/util/misc_utils.ts @@ -48,9 +48,10 @@ export function stringifyForError(value: any) { export const defaultScheduler = - (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame || // browser only - setTimeout // everything else - ).bind(global); + (() => + (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame || // browser only + setTimeout // everything else + ).bind(global))(); /** * diff --git a/packages/core/src/render3/util/view_traversal_utils.ts b/packages/core/src/render3/util/view_traversal_utils.ts index dc672ec83e..7677edf333 100644 --- a/packages/core/src/render3/util/view_traversal_utils.ts +++ b/packages/core/src/render3/util/view_traversal_utils.ts @@ -49,7 +49,7 @@ export function getRootView(componentOrLView: LView | {}): LView { */ export function findComponentView(lView: LView): LView { let rootTNode = lView[T_HOST]; - while (rootTNode && rootTNode.type === TNodeType.View) { + while (rootTNode !== null && rootTNode.type === TNodeType.View) { ngDevMode && assertDefined(lView[DECLARATION_VIEW], 'lView[DECLARATION_VIEW]'); lView = lView[DECLARATION_VIEW] !; rootTNode = lView[T_HOST]; diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index cacee5f0cc..d6fde0e553 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -19,7 +19,7 @@ import {assertDefined, assertGreaterThan, assertLessThan} from '../util/assert'; import {NodeInjector, getParentInjectorLocation} from './di'; import {addToViewTree, createEmbeddedViewAndNode, createLContainer, renderEmbeddedTemplate} from './instructions/shared'; -import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from './interfaces/container'; +import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from './interfaces/container'; import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; import {RComment, RElement, isProceduralRenderer} from './interfaces/renderer'; import {CONTEXT, LView, QUERIES, RENDERER, TView, T_HOST} from './interfaces/view'; @@ -29,7 +29,7 @@ import {getParentInjectorTNode} from './node_util'; import {getLView, getPreviousOrParentTNode} from './state'; import {getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {findComponentView} from './util/view_traversal_utils'; -import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView, viewAttachedToContainer} from './util/view_utils'; +import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView, unwrapRNode, viewAttachedToContainer} from './util/view_utils'; import {ViewRef} from './view_ref'; @@ -313,8 +313,15 @@ export function createContainerRef( lContainer = slotValue; lContainer[ACTIVE_INDEX] = -1; } else { - const commentNode = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); - ngDevMode && ngDevMode.rendererCreateComment++; + let commentNode: RComment; + // If the host is an element container, the native host element is guaranteed to be a + // comment and we can reuse that comment as anchor element for the new LContainer. + if (hostTNode.type === TNodeType.ElementContainer) { + commentNode = unwrapRNode(slotValue) as RComment; + } else { + ngDevMode && ngDevMode.rendererCreateComment++; + commentNode = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); + } // A container can be created on the root (topmost / bootstrapped) component and in this case we // can't use LTree to insert container's marker node (both parent of a comment node and the diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index e28616071e..92953d694a 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -13,7 +13,7 @@ import {renderStringify} from '../render3/util/misc_utils'; import {BypassType, allowSanitizationBypass} from './bypass'; import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; import {Sanitizer, SecurityContext} from './security'; -import {StyleSanitizeFn, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; +import {StyleSanitizeFn, StyleSanitizeMode, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; @@ -183,14 +183,22 @@ export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: * * @publicApi */ -export const ɵɵdefaultStyleSanitizer = (function(prop: string, value?: string): string | boolean { - if (value === undefined) { - return prop === 'background-image' || prop === 'background' || prop === 'border-image' || - prop === 'filter' || prop === 'list-style' || prop === 'list-style-image'; - } +export const ɵɵdefaultStyleSanitizer = + (function(prop: string, value: string|null, mode?: StyleSanitizeMode): string | boolean | null { + mode = mode || StyleSanitizeMode.ValidateAndSanitize; + let doSanitizeValue = true; + if (mode & StyleSanitizeMode.ValidateProperty) { + doSanitizeValue = prop === 'background-image' || prop === 'background' || + prop === 'border-image' || prop === 'filter' || prop === 'list-style' || + prop === 'list-style-image' || prop === 'clip-path'; + } - return ɵɵsanitizeStyle(value); -} as StyleSanitizeFn); + if (mode & StyleSanitizeMode.SanitizeOnly) { + return doSanitizeValue ? ɵɵsanitizeStyle(value) : value; + } else { + return doSanitizeValue; + } + } as StyleSanitizeFn); export function validateAgainstEventProperties(name: string) { if (name.toLowerCase().startsWith('on')) { diff --git a/packages/core/src/sanitization/style_sanitizer.ts b/packages/core/src/sanitization/style_sanitizer.ts index 16b19de093..9303a167a1 100644 --- a/packages/core/src/sanitization/style_sanitizer.ts +++ b/packages/core/src/sanitization/style_sanitizer.ts @@ -54,7 +54,7 @@ const SAFE_STYLE_VALUE = new RegExp( * Given the common use case, low likelihood of attack vector, and low impact of an attack, this * code is permissive and allows URLs that sanitize otherwise. */ -const URL_RE = /^url\(([\w\W]*)\)$/; +const URL_RE = /^url\(([^)]+)\)$/; /** * Checks that quotes (" and ') are properly balanced inside a string. Assumes @@ -103,6 +103,30 @@ export function _sanitizeStyle(value: string): string { } +/** + * A series of flags to instruct a style sanitizer to either validate + * or sanitize a value. + * + * Because sanitization is dependent on the style property (i.e. style + * sanitization for `width` is much different than for `background-image`) + * the sanitization function (e.g. `StyleSanitizerFn`) needs to check a + * property value first before it actually sanitizes any values. + * + * This enum exist to allow a style sanitization function to either only + * do validation (check the property to see whether a value will be + * sanitized or not) or to sanitize the value (or both). + * + * @publicApi + */ +export const enum StyleSanitizeMode { + /** Just check to see if the property is required to be sanitized or not */ + ValidateProperty = 0b01, + /** Skip checking the property; just sanitize the value */ + SanitizeOnly = 0b10, + /** Check the property and (if true) then sanitize the value */ + ValidateAndSanitize = 0b11, +} + /** * Used to intercept and sanitize style values before they are written to the renderer. * @@ -111,9 +135,5 @@ export function _sanitizeStyle(value: string): string { * If a value is provided then the sanitized version of that will be returned. */ export interface StyleSanitizeFn { - /** This mode is designed to instruct whether the property will be used for sanitization - * at a later point */ - (prop: string): boolean; - /** This mode is designed to sanitize the provided value */ - (prop: string, value: string): string; + (prop: string, value: string|null, mode?: StyleSanitizeMode): any; } diff --git a/packages/core/src/util/array_utils.ts b/packages/core/src/util/array_utils.ts index aac3dba3d8..ab846bbf7b 100644 --- a/packages/core/src/util/array_utils.ts +++ b/packages/core/src/util/array_utils.ts @@ -19,24 +19,23 @@ export function addAllToArray(items: any[], arr: any[]) { } /** - * Flattens an array in non-recursive way. Input arrays are not modified. + * Flattens an array. */ -export function flatten(list: any[], mapFn?: (value: any) => any): any[] { - const result: any[] = []; - let i = 0; - while (i < list.length) { - const item = list[i]; +export function flatten(list: any[], dst?: any[]): any[] { + if (dst === undefined) dst = list; + for (let i = 0; i < list.length; i++) { + let item = list[i]; if (Array.isArray(item)) { - if (item.length > 0) { - list = item.concat(list.slice(i + 1)); - i = 0; - } else { - i++; + // we need to inline it. + if (dst === list) { + // Our assumption that the list was already flat was wrong and + // we need to clone flat since we need to write to it. + dst = list.slice(0, i); } - } else { - result.push(mapFn ? mapFn(item) : item); - i++; + flatten(item, dst); + } else if (dst !== list) { + dst.push(item); } } - return result; -} + return dst; +} \ No newline at end of file diff --git a/packages/core/src/util/empty.ts b/packages/core/src/util/empty.ts index 46dc61bdf6..d1d8ce9a1b 100644 --- a/packages/core/src/util/empty.ts +++ b/packages/core/src/util/empty.ts @@ -19,6 +19,10 @@ export const EMPTY_ARRAY: any[] = []; // freezing the values prevents any code from accidentally inserting new values in if (typeof ngDevMode !== 'undefined' && ngDevMode) { + // These property accesses can be ignored because ngDevMode will be set to false + // when optimizing code and the whole if statement will be dropped. + // tslint:disable-next-line:no-toplevel-property-access Object.freeze(EMPTY_OBJ); + // tslint:disable-next-line:no-toplevel-property-access Object.freeze(EMPTY_ARRAY); } diff --git a/packages/core/src/util/global.ts b/packages/core/src/util/global.ts index 49d5b9034d..5b26299863 100644 --- a/packages/core/src/util/global.ts +++ b/packages/core/src/util/global.ts @@ -15,20 +15,16 @@ declare var global: any /** TODO #9100 */; // Not yet available in TypeScript: https://github.com/Microsoft/TypeScript/pull/29332 declare var globalThis: any /** TODO #9100 */; -function getGlobal(): any { - const __globalThis = typeof globalThis !== 'undefined' && globalThis; - const __window = typeof window !== 'undefined' && window; - const __self = typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && - self instanceof WorkerGlobalScope && self; - const __global = typeof global !== 'undefined' && global; +const __globalThis = typeof globalThis !== 'undefined' && globalThis; +const __window = typeof window !== 'undefined' && window; +const __self = typeof self !== 'undefined' && typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope && self; +const __global = typeof global !== 'undefined' && global; - // Always use __globalThis if available, which is the spec-defined global variable across all - // environments, then fallback to __global first, because in Node tests both __global and - // __window may be defined and _global should be __global in that case. - return __globalThis || __global || __window || __self; -} - -const _global = getGlobal(); +// Always use __globalThis if available, which is the spec-defined global variable across all +// environments, then fallback to __global first, because in Node tests both __global and +// __window may be defined and _global should be __global in that case. +const _global = __globalThis || __global || __window || __self; /** * Attention: whenever providing a new value, be sure to add an diff --git a/packages/core/src/util/microtask.ts b/packages/core/src/util/microtask.ts index 40edbac49d..fdc8a2b4f6 100644 --- a/packages/core/src/util/microtask.ts +++ b/packages/core/src/util/microtask.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ - -const promise: Promise = Promise.resolve(0); +const promise: Promise = (() => Promise.resolve(0))(); declare const Zone: any; diff --git a/packages/core/src/util/named_array_type.ts b/packages/core/src/util/named_array_type.ts new file mode 100644 index 0000000000..f73a00ce35 --- /dev/null +++ b/packages/core/src/util/named_array_type.ts @@ -0,0 +1,42 @@ + +/** + * @license + * Copyright Google Inc. 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 './ng_dev_mode'; +import {global} from './global'; + +/** + * THIS FILE CONTAINS CODE WHICH SHOULD BE TREE SHAKEN AND NEVER CALLED FROM PRODUCTION CODE!!! + */ + + +/** + * Creates an `Array` construction with a given name. This is useful when + * looking for memory consumption to see what time of array it is. + * + * + * @param name Name to give to the constructor + * @returns A subclass of `Array` if possible. This can only be done in + * environments which support `class` construct. + */ +export function createNamedArrayType(name: string): typeof Array { + // This should never be called in prod mode, so let's verify that is the case. + if (ngDevMode) { + try { + // We need to do it this way so that TypeScript does not down-level the below code. + const FunctionConstructor: any = createNamedArrayType.constructor; + return (new FunctionConstructor('Array', `return class ABC extends Array{}`))(Array); + } catch (e) { + // If it does not work just give up and fall back to regular Array. + return Array; + } + } else { + throw new Error( + 'Looks like we are in \'prod mode\', but we are creating a named Array type, which is wrong! Check your code'); + } +} \ No newline at end of file diff --git a/packages/core/src/util/ng_dev_mode.ts b/packages/core/src/util/ng_dev_mode.ts index d70d65c44a..f0424c44b3 100644 --- a/packages/core/src/util/ng_dev_mode.ts +++ b/packages/core/src/util/ng_dev_mode.ts @@ -11,6 +11,7 @@ import {global} from './global'; declare global { const ngDevMode: null|NgDevModePerfCounters; interface NgDevModePerfCounters { + namedConstructors: boolean; firstTemplatePass: number; tNode: number; tView: number; @@ -45,7 +46,9 @@ declare global { } export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { + const locationString = typeof location !== 'undefined' ? location.toString() : ''; const newCounters: NgDevModePerfCounters = { + namedConstructors: locationString.indexOf('ngDevMode=namedConstructors') != -1, firstTemplatePass: 0, tNode: 0, tView: 0, @@ -79,7 +82,8 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { }; // Make sure to refer to ngDevMode as ['ngDevMode'] for closure. - global['ngDevMode'] = newCounters; + const allowNgDevModeTrue = locationString.indexOf('ngDevMode=false') === -1; + global['ngDevMode'] = allowNgDevModeTrue && newCounters; return newCounters; } diff --git a/packages/core/src/util/ng_i18n_closure_mode.ts b/packages/core/src/util/ng_i18n_closure_mode.ts index be1231060d..a966565819 100644 --- a/packages/core/src/util/ng_i18n_closure_mode.ts +++ b/packages/core/src/util/ng_i18n_closure_mode.ts @@ -16,8 +16,15 @@ declare global { * NOTE: changes to the `ngI18nClosureMode` name must be synced with `compiler-cli/src/tooling.ts`. */ if (typeof ngI18nClosureMode === 'undefined') { + // These property accesses can be ignored because ngI18nClosureMode will be set to false + // when optimizing code and the whole if statement will be dropped. // Make sure to refer to ngI18nClosureMode as ['ngI18nClosureMode'] for closure. - global['ngI18nClosureMode'] = - // TODO(FW-1250): validate that this actually, you know, works. - typeof goog !== 'undefined' && typeof goog.getMsg === 'function'; + // NOTE: we need to have it in IIFE so that the tree-shaker is happy. + (function() { + // tslint:disable-next-line:no-toplevel-property-access + global['ngI18nClosureMode'] = + // TODO(FW-1250): validate that this actually, you know, works. + // tslint:disable-next-line:no-toplevel-property-access + typeof goog !== 'undefined' && typeof goog.getMsg === 'function'; + })(); } diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index 99fa8a82cc..bc1d670deb 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/animations/browser", "//packages/animations/browser/testing", "//packages/common", + "//packages/common/locales", "//packages/compiler", "//packages/compiler/testing", "//packages/core", diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel index 8f74f3f99a..e87138061b 100644 --- a/packages/core/test/acceptance/BUILD.bazel +++ b/packages/core/test/acceptance/BUILD.bazel @@ -1,6 +1,6 @@ package(default_visibility = ["//visibility:private"]) -load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library", "ts_web_test_suite") ts_library( name = "acceptance_lib", @@ -9,7 +9,11 @@ ts_library( ["**/*.ts"], ), deps = [ + "//packages/animations", + "//packages/animations/browser", + "//packages/animations/browser/testing", "//packages/common", + "//packages/common/locales", "//packages/compiler", "//packages/compiler/testing", "//packages/core", @@ -17,8 +21,12 @@ ts_library( "//packages/core/testing", "//packages/platform-browser", "//packages/platform-browser-dynamic", + "//packages/platform-browser/animations", "//packages/platform-browser/testing", + "//packages/platform-server", "//packages/private/testing", + "//packages/router", + "@npm//rxjs", "@npm//zone.js", ], ) @@ -34,3 +42,10 @@ jasmine_node_test( "@npm//zone.js", ], ) + +ts_web_test_suite( + name = "acceptance_web", + deps = [ + ":acceptance_lib", + ], +) diff --git a/packages/core/test/acceptance/attributes_spec.ts b/packages/core/test/acceptance/attributes_spec.ts index d8946033a0..1773227951 100644 --- a/packages/core/test/acceptance/attributes_spec.ts +++ b/packages/core/test/acceptance/attributes_spec.ts @@ -98,3 +98,54 @@ describe('attribute binding', () => { expect(a.href.indexOf('unsafe:')).toBe(-1); }); }); + +describe('attribute interpolation', () => { + it('should handle all varieties of interpolation', () => { + @Component({ + template: ` +
+
+
+
+
+
+
+
+
+
+ ` + }) + class App { + a = 1; + b = 2; + c = 3; + d = 4; + e = 5; + f = 6; + g = 7; + h = 8; + i = 9; + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const divs = fixture.debugElement.queryAll(By.css('div[title]')); + + expect(divs.map(el => el.nativeElement.getAttribute('title'))).toEqual([ + 'a1b2c3d4e5f6g7h8i9j', + 'a1b2c3d4e5f6g7h8i', + 'a1b2c3d4e5f6g7h', + 'a1b2c3d4e5f6g', + 'a1b2c3d4e5f', + 'a1b2c3d4e', + 'a1b2c3d', + 'a1b2c', + 'a1b', + '1', + ]); + }); +}); diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 340a224321..59aa0c8f96 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -7,7 +7,8 @@ */ -import {ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Directive, EmbeddedViewRef, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, Input, NgModule, OnInit, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -79,7 +80,7 @@ describe('change detection', () => { }) class TestCmpt { counter = 0; - @ViewChild('vc', {read: ViewContainerRef}) vcRef !: ViewContainerRef; + @ViewChild('vc', {read: ViewContainerRef, static: false}) vcRef !: ViewContainerRef; constructor(private _cfr: ComponentFactoryResolver) {} @@ -127,4 +128,853 @@ describe('change detection', () => { }); }); -}); \ No newline at end of file + describe('OnPush', () => { + @Component({ + selector: 'my-comp', + changeDetection: ChangeDetectionStrategy.OnPush, + template: `{{ doCheckCount }} - {{ name }} ` + }) + class MyComponent implements DoCheck { + @Input() + name = 'Nancy'; + doCheckCount = 0; + + ngDoCheck(): void { this.doCheckCount++; } + + onClick() {} + } + + @Component({selector: 'my-app', template: ''}) + class MyApp { + @ViewChild(MyComponent, {static: false}) comp !: MyComponent; + name: string = 'Nancy'; + } + + it('should check OnPush components on initialization', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should call doCheck even when OnPush components are not dirty', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(3); + }); + + it('should skip OnPush components in update mode when they are not dirty', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty. + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + fixture.detectChanges(); + + // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty. + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should check OnPush components in update mode when inputs change', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.componentInstance.name = 'Bess'; + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + // View should update, as changed input marks view dirty + expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Bess'); + + fixture.componentInstance.name = 'George'; + fixture.detectChanges(); + + // View should update, as changed input marks view dirty + expect(fixture.componentInstance.comp.doCheckCount).toEqual(3); + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(4); + // View should not be updated to "4", as inputs have not changed. + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - George'); + }); + + it('should check OnPush components in update mode when component events occur', () => { + TestBed.configureTestingModule({declarations: [MyComponent, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + const button = fixture.nativeElement.querySelector('button') !; + button.click(); + + // No ticks should have been scheduled. + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + fixture.detectChanges(); + + // Because the onPush comp should be dirty, it should update once CD runs + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('2 - Nancy'); + }); + + it('should not check OnPush components in update mode when parent events occur', () => { + @Component({ + selector: 'button-parent', + template: '' + }) + class ButtonParent { + @ViewChild(MyComponent, {static: false}) comp !: MyComponent; + noop() {} + } + + TestBed.configureTestingModule({declarations: [MyComponent, ButtonParent]}); + const fixture = TestBed.createComponent(ButtonParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button#parent'); + button.click(); + fixture.detectChanges(); + + // The comp should still be clean. So doCheck will run, but the view should display 1. + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - Nancy'); + }); + + it('should check parent OnPush components in update mode when child events occur', () => { + @Component({ + selector: 'button-parent', + template: '{{ doCheckCount }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class ButtonParent implements DoCheck { + @ViewChild(MyComponent, {static: false}) comp !: MyComponent; + noop() {} + + doCheckCount = 0; + ngDoCheck(): void { this.doCheckCount++; } + } + + @Component({selector: 'my-button-app', template: ''}) + class MyButtonApp { + @ViewChild(ButtonParent, {static: false}) parent !: ButtonParent; + } + + TestBed.configureTestingModule({declarations: [MyButtonApp, MyComponent, ButtonParent]}); + const fixture = TestBed.createComponent(MyButtonApp); + fixture.detectChanges(); + + const parent = fixture.componentInstance.parent; + const comp = parent.comp; + + expect(parent.doCheckCount).toEqual(1); + expect(comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy'); + + fixture.detectChanges(); + expect(parent.doCheckCount).toEqual(2); + // parent isn't checked, so child doCheck won't run + expect(comp.doCheckCount).toEqual(1); + expect(fixture.nativeElement.textContent.trim()).toEqual('1 - 1 - Nancy'); + + const button = fixture.nativeElement.querySelector('button'); + button.click(); + + // No ticks should have been scheduled. + expect(parent.doCheckCount).toEqual(2); + expect(comp.doCheckCount).toEqual(1); + + fixture.detectChanges(); + expect(parent.doCheckCount).toEqual(3); + expect(comp.doCheckCount).toEqual(2); + expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy'); + }); + + }); + + describe('ChangeDetectorRef', () => { + describe('detectChanges()', () => { + @Component({ + selector: 'my-comp', + template: '{{ name }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class MyComp implements DoCheck { + doCheckCount = 0; + name = 'Nancy'; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({selector: 'parent-comp', template: `{{ doCheckCount}} - `}) + class ParentComp implements DoCheck { + @ViewChild(MyComp, {static: false}) myComp !: MyComp; + + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Directive({selector: '[dir]'}) + class Dir { + constructor(public cdr: ChangeDetectorRef) {} + } + + it('should check the component view when called by component (even when OnPush && clean)', + () => { + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Nancy'); + + fixture.componentInstance.name = + 'Bess'; // as this is not an Input, the component stays clean + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Bess'); + }); + + it('should NOT call component doCheck when called by a component', () => { + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you + // may see doCheck called in some cases bc of the extra CD run triggered by zone.js. + // It's important not to call doCheck to allow calls to detectChanges in that hook. + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + }); + + it('should NOT check the component parent when called by a child component', () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + + fixture.componentInstance.doCheckCount = 100; + fixture.componentInstance.myComp.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(100); + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + }); + + it('should check component children when called by component if dirty or check-always', + () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + fixture.componentInstance.myComp.name = 'Bess'; + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.doCheckCount).toEqual(1); + expect(fixture.componentInstance.myComp.doCheckCount).toEqual(2); + // OnPush child is not dirty, so its change isn't rendered. + expect(fixture.nativeElement.textContent).toEqual('1 - Nancy'); + }); + + it('should not group detectChanges calls (call every time)', () => { + TestBed.configureTestingModule({declarations: [MyComp, ParentComp]}); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.doCheckCount).toEqual(1); + + fixture.componentInstance.cdr.detectChanges(); + fixture.componentInstance.cdr.detectChanges(); + expect(fixture.componentInstance.myComp.doCheckCount).toEqual(3); + }); + + it('should check component view when called by directive on component node', () => { + @Component({template: ''}) + class MyApp { + @ViewChild(MyComp, {static: false}) myComp !: MyComp; + @ViewChild(Dir, {static: false}) dir !: Dir; + } + + TestBed.configureTestingModule({declarations: [MyComp, Dir, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Nancy'); + + fixture.componentInstance.myComp.name = 'George'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('George'); + }); + + it('should check host component when called by directive on element node', () => { + @Component({template: '{{ value }}
'}) + class MyApp { + @ViewChild(MyComp, {static: false}) myComp !: MyComp; + @ViewChild(Dir, {static: false}) dir !: Dir; + value = ''; + } + + TestBed.configureTestingModule({declarations: [Dir, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + fixture.componentInstance.value = 'Frank'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Frank'); + + fixture.componentInstance.value = 'Joe'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Joe'); + }); + + it('should check the host component when called from EmbeddedViewRef', () => { + @Component({template: '{{ name }}
'}) + class MyApp { + @ViewChild(Dir, {static: false}) dir !: Dir; + showing = true; + name = 'Amelia'; + } + + TestBed.configureTestingModule({declarations: [Dir, MyApp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('Amelia'); + + fixture.componentInstance.name = 'Emerson'; + fixture.componentInstance.dir.cdr.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('Emerson'); + }); + + it('should support call in ngOnInit', () => { + @Component({template: '{{ value }}'}) + class DetectChangesComp implements OnInit { + value = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.value++; + this.cdr.detectChanges(); + } + } + + TestBed.configureTestingModule({declarations: [DetectChangesComp]}); + const fixture = TestBed.createComponent(DetectChangesComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1'); + }); + + ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => { + it(`should not go infinite loop when recursively called from children's ng${hook}`, () => { + @Component({template: ''}) + class ParentComp { + constructor(public cdr: ChangeDetectorRef) {} + triggerChangeDetection() { this.cdr.detectChanges(); } + } + + @Component({template: '{{inp}}', selector: 'child-comp'}) + class ChildComp { + @Input() + inp: any = ''; + + count = 0; + constructor(public parentComp: ParentComp) {} + + ngOnInit() { this.check('OnInit'); } + ngAfterContentInit() { this.check('AfterContentInit'); } + ngAfterViewInit() { this.check('AfterViewInit'); } + ngOnChanges() { this.check('OnChanges'); } + + check(h: string) { + if (h === hook) { + this.count++; + if (this.count > 1) throw new Error(`ng${hook} should be called only once!`); + this.parentComp.triggerChangeDetection(); + } + } + } + + TestBed.configureTestingModule({declarations: [ParentComp, ChildComp]}); + + expect(() => { + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + }).not.toThrow(); + }); + }); + + it('should support call in ngDoCheck', () => { + @Component({template: '{{doCheckCount}}'}) + class DetectChangesComp { + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { + this.doCheckCount++; + this.cdr.detectChanges(); + } + } + + TestBed.configureTestingModule({declarations: [DetectChangesComp]}); + const fixture = TestBed.createComponent(DetectChangesComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('1'); + }); + + describe('dynamic views', () => { + @Component({selector: 'structural-comp', template: '{{ value }}'}) + class StructuralComp { + @Input() + tmp !: TemplateRef; + value = 'one'; + + constructor(public vcr: ViewContainerRef) {} + + create() { return this.vcr.createEmbeddedView(this.tmp, {ctx: this}); } + } + + it('should support ViewRef.detectChanges()', () => { + @Component({ + template: + '{{ ctx.value }}' + }) + class App { + @ViewChild(StructuralComp, {static: false}) structuralComp !: StructuralComp; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + const viewRef: EmbeddedViewRef = fixture.componentInstance.structuralComp.create(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('oneone'); + + // check embedded view update + fixture.componentInstance.structuralComp.value = 'two'; + viewRef.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('onetwo'); + + // check root view update + fixture.componentInstance.structuralComp.value = 'three'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('threethree'); + }); + + it('should support ViewRef.detectChanges() directly after creation', () => { + @Component({ + template: 'Template text' + }) + class App { + @ViewChild(StructuralComp, {static: false}) structuralComp !: StructuralComp; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + const viewRef: EmbeddedViewRef = fixture.componentInstance.structuralComp.create(); + viewRef.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('oneTemplate text'); + }); + + }); + + }); + + describe('attach/detach', () => { + @Component({selector: 'detached-comp', template: '{{ value }}'}) + class DetachedComp implements DoCheck { + value = 'one'; + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({template: ''}) + class MyApp { + @ViewChild(DetachedComp, {static: false}) comp !: DetachedComp; + + constructor(public cdr: ChangeDetectorRef) {} + } + + it('should not check detached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + }); + + it('should check re-attached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + fixture.componentInstance.comp.value = 'two'; + + fixture.componentInstance.comp.cdr.reattach(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should call lifecycle hooks on detached components', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.componentInstance.comp.doCheckCount).toEqual(1); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.detectChanges(); + expect(fixture.componentInstance.comp.doCheckCount).toEqual(2); + }); + + it('should check detached component when detectChanges is called', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.comp.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.componentInstance.comp.cdr.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should not check detached component when markDirty is called', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + const comp = fixture.componentInstance.comp; + + comp.cdr.detach(); + comp.value = 'two'; + comp.cdr.markForCheck(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + }); + + it('should detach any child components when parent is detached', () => { + TestBed.configureTestingModule({declarations: [MyApp, DetachedComp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.cdr.detach(); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.cdr.reattach(); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + it('should detach OnPush components properly', () => { + + @Component({ + selector: 'on-push-comp', + template: '{{ value }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushComp { + @Input() + value !: string; + + constructor(public cdr: ChangeDetectorRef) {} + } + + @Component({template: ''}) + class OnPushApp { + @ViewChild(OnPushComp, {static: false}) onPushComp !: OnPushComp; + value = ''; + } + + TestBed.configureTestingModule({declarations: [OnPushApp, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushApp); + fixture.detectChanges(); + + fixture.componentInstance.value = 'one'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.onPushComp.cdr.detach(); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one'); + + fixture.componentInstance.onPushComp.cdr.reattach(); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two'); + }); + + }); + + describe('markForCheck()', () => { + @Component({ + selector: 'on-push-comp', + template: '{{ value }}', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushComp implements DoCheck { + value = 'one'; + + doCheckCount = 0; + + constructor(public cdr: ChangeDetectorRef) {} + + ngDoCheck() { this.doCheckCount++; } + } + + @Component({ + template: '{{ value }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class OnPushParent { + @ViewChild(OnPushComp, {static: false}) comp !: OnPushComp; + value = 'one'; + } + + it('should ensure OnPush components are checked', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + + // Change detection should not have run yet, since markForCheck + // does not itself schedule change detection. + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + }); + + it('should never schedule change detection on its own', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + const comp = fixture.componentInstance.comp; + + expect(comp.doCheckCount).toEqual(1); + + comp.cdr.markForCheck(); + comp.cdr.markForCheck(); + + expect(comp.doCheckCount).toEqual(1); + }); + + it('should ensure ancestor OnPush components are checked', () => { + TestBed.configureTestingModule({declarations: [OnPushParent, OnPushComp]}); + const fixture = TestBed.createComponent(OnPushParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two - one'); + + }); + + it('should ensure OnPush components in embedded views are checked', () => { + @Component({ + template: '{{ value }} - ', + changeDetection: ChangeDetectionStrategy.OnPush + }) + class EmbeddedViewParent { + @ViewChild(OnPushComp, {static: false}) comp !: OnPushComp; + value = 'one'; + showing = true; + } + + TestBed.configureTestingModule( + {declarations: [EmbeddedViewParent, OnPushComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(EmbeddedViewParent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.componentInstance.comp.cdr.markForCheck(); + // markForCheck should not trigger change detection on its own. + expect(fixture.nativeElement.textContent).toEqual('one - one'); + + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + + fixture.componentInstance.value = 'two'; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('one - two'); + + fixture.componentInstance.comp.cdr.markForCheck(); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toEqual('two - two'); + }); + + // TODO(kara): add test for dynamic views once bug fix is in + }); + + describe('checkNoChanges', () => { + let comp: NoChangesComp; + + @Component({selector: 'no-changes-comp', template: '{{ value }}'}) + class NoChangesComp { + value = 1; + doCheckCount = 0; + contentCheckCount = 0; + viewCheckCount = 0; + + ngDoCheck() { this.doCheckCount++; } + + ngAfterContentChecked() { this.contentCheckCount++; } + + ngAfterViewChecked() { this.viewCheckCount++; } + + constructor(public cdr: ChangeDetectorRef) { comp = this; } + } + + @Component({template: '{{ value }} - '}) + class AppComp { + value = 1; + + constructor(public cdr: ChangeDetectorRef) {} + } + + // Custom error handler that just rethrows all the errors from the + // view, rather than logging them out. Used to keep our logs clean. + class RethrowErrorHandler extends ErrorHandler { + handleError(error: any) { throw error; } + } + + it('should throw if bindings in current view have changed', () => { + TestBed.configureTestingModule({ + declarations: [NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(NoChangesComp); + + expect(() => { fixture.componentInstance.cdr.checkNoChanges(); }) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi); + }); + + it('should throw if interpolations in current view have changed', () => { + TestBed.configureTestingModule({ + declarations: [AppComp, NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(AppComp); + + expect(() => fixture.componentInstance.cdr.checkNoChanges()) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*1'/gi); + }); + + it('should throw if bindings in embedded view have changed', () => { + @Component({template: '{{ showing }}'}) + class EmbeddedViewApp { + showing = true; + constructor(public cdr: ChangeDetectorRef) {} + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewApp], + imports: [CommonModule], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + const fixture = TestBed.createComponent(EmbeddedViewApp); + + expect(() => fixture.componentInstance.cdr.checkNoChanges()) + .toThrowError( + /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '.*undefined'. Current value: '.*true'/gi); + }); + + it('should NOT call lifecycle hooks', () => { + TestBed.configureTestingModule({ + declarations: [AppComp, NoChangesComp], + providers: [{provide: ErrorHandler, useClass: RethrowErrorHandler}] + }); + + const fixture = TestBed.createComponent(AppComp); + fixture.detectChanges(); + + expect(comp.doCheckCount).toEqual(1); + expect(comp.contentCheckCount).toEqual(1); + expect(comp.viewCheckCount).toEqual(1); + + comp.value = 2; + expect(() => fixture.componentInstance.cdr.checkNoChanges()).toThrow(); + expect(comp.doCheckCount).toEqual(1); + expect(comp.contentCheckCount).toEqual(1); + expect(comp.viewCheckCount).toEqual(1); + }); + + }); + + }); + +}); diff --git a/packages/core/test/acceptance/common_integration_spec.ts b/packages/core/test/acceptance/common_integration_spec.ts new file mode 100644 index 0000000000..14615fb8f6 --- /dev/null +++ b/packages/core/test/acceptance/common_integration_spec.ts @@ -0,0 +1,541 @@ +/** + * @license + * Copyright Google Inc. 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 {Component, Directive} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('@angular/common integration', () => { + + describe('NgForOf', () => { + @Directive({selector: '[dir]'}) + class MyDirective { + } + + @Component({selector: 'app-child', template: '
comp text
'}) + class ChildComponent { + } + + @Component({selector: 'app-root', template: ''}) + class AppComponent { + items: string[] = ['first', 'second']; + } + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [AppComponent, ChildComponent, MyDirective]}); + }); + + it('should update a loop', () => { + TestBed.overrideTemplate( + AppComponent, '
  • {{item}}
'); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // change detection cycle, no model changes + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first', 'second']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['first']); + + // change an item + items[0] = 'one'; + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one']); + + // add an item + items.push('two'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['one', 'two']); + }); + + it('should support ngForOf context variables', () => { + TestBed.overrideTemplate( + AppComponent, + '
  • {{myIndex}} of {{myCount}}: {{item}}
'); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual(['0 of 2: first', '1 of 2: second']); + + // add an item in the middle + const items = fixture.componentInstance.items; + items.splice(1, 0, 'middle'); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.map(li => li.textContent)).toEqual([ + '0 of 3: first', '1 of 3: middle', '2 of 3: second' + ]); + }); + + it('should instantiate directives inside directives properly in an ngFor', () => { + TestBed.overrideTemplate(AppComponent, ''); + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + + const children = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + // expect 2 children, each one with a directive + expect(children.length).toBe(2); + expect(children.map(child => child.nativeElement.innerHTML)).toEqual([ + '
comp text
', '
comp text
' + ]); + let directive = children[0].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + directive = children[1].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + + // add an item + const items = fixture.componentInstance.items; + items.push('third'); + fixture.detectChanges(); + + const childrenAfterAdd = fixture.debugElement.queryAll(By.directive(ChildComponent)); + + expect(childrenAfterAdd.length).toBe(3); + expect(childrenAfterAdd.map(child => child.nativeElement.innerHTML)).toEqual([ + '
comp text
', '
comp text
', '
comp text
' + ]); + directive = childrenAfterAdd[2].query(By.directive(MyDirective)); + expect(directive).not.toBeNull(); + }); + + it('should retain parent view listeners when the NgFor destroy views', () => { + @Component({ + selector: 'app-toggle', + template: ` +
    +
  • {{item}}
  • +
` + }) + class ToggleComponent { + private _data: number[] = [1, 2, 3]; + items: number[] = []; + + toggle() { + if (this.items.length) { + this.items = []; + } else { + this.items = this._data; + } + } + } + + TestBed.configureTestingModule({declarations: [ToggleComponent]}); + const fixture = TestBed.createComponent(ToggleComponent); + fixture.detectChanges(); + + // no elements in the list + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // this will fill the list + fixture.componentInstance.toggle(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + expect(listItems.map(li => li.textContent)).toEqual(['1', '2', '3']); + + // now toggle via the button + const button: HTMLButtonElement = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(0); + + // toggle again + button.click(); + fixture.detectChanges(); + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(3); + }); + + it('should support multiple levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `
    +
  • + {{cell}} - {{ row.value }} - {{ items.length }} +
  • +
` + }) + class MultiLevelComponent { + items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; + } + + TestBed.configureTestingModule({declarations: [MultiLevelComponent]}); + const fixture = TestBed.createComponent(MultiLevelComponent); + fixture.detectChanges(); + + // change detection cycle, no model changes + let listItems = + Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + let spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['3 - second - 2', '4 - second - 2']); + + // remove the last item + const items = fixture.componentInstance.items; + items.length = 1; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['1 - first - 1', '2 - first - 1']); + + // change an item + items[0].data[0] = 'one'; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(1); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 1', '2 - first - 1']); + + // add an item + items[1] = {data: ['three', '4'], value: 'third'}; + fixture.detectChanges(); + + listItems = Array.from((fixture.nativeElement as HTMLUListElement).querySelectorAll('li')); + expect(listItems.length).toBe(2); + spanItems = Array.from(listItems[0].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual(['one - first - 2', '2 - first - 2']); + spanItems = Array.from(listItems[1].querySelectorAll('span')); + expect(spanItems.map(span => span.textContent)).toEqual([ + 'three - third - 2', '4 - third - 2' + ]); + }); + + it('should support multiple levels of embedded templates with listeners', () => { + @Component({ + selector: 'app-multi', + template: `
+

+ + {{ row.value }} - {{ name }} +

+
` + }) + class MultiLevelWithListenerComponent { + items: any[] = [{data: ['1'], value: 'first'}]; + name = 'app'; + events: string[] = []; + + onClick(value: string, name: string) { this.events.push(value, name); } + } + + TestBed.configureTestingModule({declarations: [MultiLevelWithListenerComponent]}); + const fixture = TestBed.createComponent(MultiLevelWithListenerComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('p'); + expect(elements.length).toBe(1); + expect(elements[0].innerHTML).toBe(' first - app '); + + const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app']); + + fixture.componentInstance.name = 'new name'; + fixture.detectChanges(); + expect(elements[0].innerHTML).toBe(' first - new name '); + + span.click(); + expect(fixture.componentInstance.events).toEqual(['first', 'app', 'first', 'new name']); + }); + + it('should support skipping contexts', () => { + @Component({ + selector: 'app-multi', + template: `
+
+ {{ cell.value }} - {{ name }} +
+
` + }) + class SkippingContextComponent { + name = 'app'; + items: any[] = [ + [ + // row + {value: 'one', data: ['1', '2']} // cell + ], + [{value: 'two', data: ['3', '4']}] + ]; + } + + TestBed.configureTestingModule({declarations: [SkippingContextComponent]}); + const fixture = TestBed.createComponent(SkippingContextComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('span'); + expect(elements.length).toBe(4); + expect(elements[0].textContent).toBe('one - app'); + expect(elements[1].textContent).toBe('one - app'); + expect(elements[2].textContent).toBe('two - app'); + expect(elements[3].textContent).toBe('two - app'); + + fixture.componentInstance.name = 'other'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('one - other'); + expect(elements[1].textContent).toBe('one - other'); + expect(elements[2].textContent).toBe('two - other'); + expect(elements[3].textContent).toBe('two - other'); + }); + + it('should support context for 9+ levels of embedded templates', () => { + @Component({ + selector: 'app-multi', + template: `
+ + + + + + + + {{ item8 }}.{{ item7.value }}.{{ item6.value }}.{{ item5.value }}.{{ item4.value }}.{{ item3.value }}.{{ item2.value }}.{{ item1.value }}.{{ item0.value }}.{{ value }} + + + + + + + +
` + }) + class NineLevelsComponent { + value = 'App'; + items: any[] = [ + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '1', '2' // item8 + ], + value: 'h' + }], + value: 'g' + }], + value: 'f' + }], + value: 'e' + }], + value: 'd' + }], + value: 'c' + }], + value: 'b' + }], + value: 'a' + }, + { + // item0 + data: [{ + // item1 + data: [{ + // item2 + data: [{ + // item3 + data: [{ + // item4 + data: [{ + // item5 + data: [{ + // item6 + data: [{ + // item7 + data: [ + '3', '4' // item8 + ], + value: 'H' + }], + value: 'G' + }], + value: 'F' + }], + value: 'E' + }], + value: 'D' + }], + value: 'C' + }], + value: 'B' + }], + value: 'A' + } + ]; + } + + TestBed.configureTestingModule({declarations: [NineLevelsComponent]}); + const fixture = TestBed.createComponent(NineLevelsComponent); + fixture.detectChanges(); + + const divItems = (fixture.nativeElement as HTMLElement).querySelectorAll('div'); + expect(divItems.length).toBe(2); // 2 outer loops + let spanItems = + divItems[0].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('1.h.g.f.e.d.c.b.a.App'); + expect(spanItems[1].textContent).toBe('2.h.g.f.e.d.c.b.a.App'); + spanItems = + divItems[1].querySelectorAll('span > span > span > span > span > span > span > span'); + expect(spanItems.length).toBe(2); // 2 inner elements + expect(spanItems[0].textContent).toBe('3.H.G.F.E.D.C.B.A.App'); + expect(spanItems[1].textContent).toBe('4.H.G.F.E.D.C.B.A.App'); + }); + }); + + describe('ngIf', () => { + it('should support sibling ngIfs', () => { + @Component({ + selector: 'app-multi', + template: ` +
{{ valueOne }}
+
{{ valueTwo }}
+ ` + }) + class SimpleConditionComponent { + showing = true; + valueOne = 'one'; + valueTwo = 'two'; + } + + TestBed.configureTestingModule({declarations: [SimpleConditionComponent]}); + const fixture = TestBed.createComponent(SimpleConditionComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(2); + expect(elements[0].textContent).toBe('one'); + expect(elements[1].textContent).toBe('two'); + + fixture.componentInstance.valueOne = '$$one$$'; + fixture.componentInstance.valueTwo = '$$two$$'; + fixture.detectChanges(); + expect(elements[0].textContent).toBe('$$one$$'); + expect(elements[1].textContent).toBe('$$two$$'); + }); + + it('should handle nested ngIfs with no intermediate context vars', () => { + @Component({ + selector: 'app-multi', + template: `
+
+
{{ name }}
+
+
+ ` + }) + class NestedConditionsComponent { + showing = true; + outerShowing = true; + innerShowing = true; + name = 'App name'; + } + + TestBed.configureTestingModule({declarations: [NestedConditionsComponent]}); + const fixture = TestBed.createComponent(NestedConditionsComponent); + fixture.detectChanges(); + + const elements = fixture.nativeElement.querySelectorAll('div'); + expect(elements.length).toBe(3); + expect(elements[2].textContent).toBe('App name'); + + fixture.componentInstance.name = 'Other name'; + fixture.detectChanges(); + expect(elements[2].textContent).toBe('Other name'); + }); + }); + + describe('NgTemplateOutlet', () => { + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class EmbeddedViewsComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [EmbeddedViewsComponent]}); + const fixture = TestBed.createComponent(EmbeddedViewsComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + + it('should create and remove embedded views', () => { + @Component({ + selector: 'app-multi', + template: `from tpl + + ` + }) + class NgContainerComponent { + showing = false; + } + + TestBed.configureTestingModule({declarations: [NgContainerComponent]}); + const fixture = TestBed.createComponent(NgContainerComponent); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toBe('from tpl'); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).not.toBe('from tpl'); + }); + }); +}); diff --git a/packages/core/test/acceptance/component_spec.ts b/packages/core/test/acceptance/component_spec.ts index 940e1dc5f1..7fe1e54939 100644 --- a/packages/core/test/acceptance/component_spec.ts +++ b/packages/core/test/acceptance/component_spec.ts @@ -70,7 +70,7 @@ describe('component', () => { entryComponents: [OtherComponent] }) class TestComponent { - @ViewChild('vc', {read: ViewContainerRef}) vcref !: ViewContainerRef; + @ViewChild('vc', {read: ViewContainerRef, static: true}) vcref !: ViewContainerRef; constructor(private _cfr: ComponentFactoryResolver) {} diff --git a/packages/core/test/acceptance/content_spec.ts b/packages/core/test/acceptance/content_spec.ts index c84aa1d5fc..5673111585 100644 --- a/packages/core/test/acceptance/content_spec.ts +++ b/packages/core/test/acceptance/content_spec.ts @@ -245,7 +245,7 @@ describe('projection', () => { @Component( {selector: 'comp', template: ``}) class Comp { - @ViewChild(TemplateRef) template !: TemplateRef; + @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; } @Directive({selector: '[trigger]'}) diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index f0baec7bc8..cdb9c2bc63 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,10 +7,10 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, Host, HostBinding, INJECTOR, Inject, Injectable, Injector, Input, LOCALE_ID, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, forwardRef} from '@angular/core'; +import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, Host, HostBinding, INJECTOR, Inject, Injectable, InjectionToken, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, forwardRef} from '@angular/core'; import {ViewRef} from '@angular/core/src/render3/view_ref'; import {TestBed} from '@angular/core/testing'; -import {onlyInIvy} from '@angular/private/testing'; +import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; describe('di', () => { describe('no dependencies', () => { @@ -31,6 +31,32 @@ describe('di', () => { }); }); + describe('multi providers', () => { + it('should process ModuleWithProvider providers after module imports', () => { + const testToken = new InjectionToken('test-multi'); + + @NgModule({providers: [{provide: testToken, useValue: 'A', multi: true}]}) + class TestModuleA { + } + + @NgModule({providers: [{provide: testToken, useValue: 'B', multi: true}]}) + class TestModuleB { + } + + TestBed.configureTestingModule({ + imports: [ + { + ngModule: TestModuleA, + providers: [{provide: testToken, useValue: 'C', multi: true}], + }, + TestModuleB, + ] + }); + + expect(TestBed.get(testToken) as string[]).toEqual(['A', 'B', 'C']); + }); + }); + describe('directive injection', () => { let log: string[] = []; @@ -370,14 +396,14 @@ describe('di', () => {
{{ dir.dirB.value }}
- +
` }) class MyComp { - @ViewChild(StructuralDirective) structuralDir !: StructuralDirective; + @ViewChild(StructuralDirective, {static: false}) structuralDir !: StructuralDirective; } TestBed.configureTestingModule( @@ -421,8 +447,8 @@ describe('di', () => {
` }) class MyApp { - @ViewChild(HostBindingDirective) hostBindingDir !: HostBindingDirective; - @ViewChild(DirectiveA) dirA !: DirectiveA; + @ViewChild(HostBindingDirective, {static: false}) hostBindingDir !: HostBindingDirective; + @ViewChild(DirectiveA, {static: false}) dirA !: DirectiveA; } TestBed.configureTestingModule( @@ -535,7 +561,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(DirectiveA) dirA !: DirectiveA; + @ViewChild(DirectiveA, {static: false}) dirA !: DirectiveA; } TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); @@ -555,7 +581,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(DirectiveC) dirC !: DirectiveC; + @ViewChild(DirectiveC, {static: false}) dirC !: DirectiveC; } TestBed.configureTestingModule({declarations: [DirectiveC, MyComp]}); @@ -575,7 +601,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(DirectiveC) dirC !: DirectiveC; + @ViewChild(DirectiveC, {static: false}) dirC !: DirectiveC; } TestBed.configureTestingModule({declarations: [DirectiveB, DirectiveC, MyComp]}); @@ -596,12 +622,12 @@ describe('di', () => { @Component({selector: 'my-comp', template: '
'}) class MyComp { - @ViewChild(DirectiveA) dirA !: DirectiveA; + @ViewChild(DirectiveA, {static: false}) dirA !: DirectiveA; } @Component({template: ''}) class MyApp { - @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(MyComp, {static: false}) myComp !: MyComp; } TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); @@ -646,12 +672,12 @@ describe('di', () => { viewProviders: [{provide: String, useValue: 'Foo'}] }) class MyComp { - @ViewChild(DirectiveString) dirString !: DirectiveString; + @ViewChild(DirectiveString, {static: false}) dirString !: DirectiveString; } @Component({template: ''}) class MyApp { - @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(MyComp, {static: false}) myComp !: MyComp; } TestBed.configureTestingModule({declarations: [DirectiveString, MyComp, MyApp]}); @@ -670,12 +696,12 @@ describe('di', () => { @Component({selector: 'my-comp', template: '
'}) class MyComp { - @ViewChild(DirectiveComp) dirComp !: DirectiveComp; + @ViewChild(DirectiveComp, {static: false}) dirComp !: DirectiveComp; } @Component({template: ''}) class MyApp { - @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(MyComp, {static: false}) myComp !: MyComp; } TestBed.configureTestingModule({declarations: [DirectiveComp, MyComp, MyApp]}); @@ -734,7 +760,7 @@ describe('di', () => { @Component({template: ''}) class MyApp { - @ViewChild(MyComp) myComp !: MyComp; + @ViewChild(MyComp, {static: false}) myComp !: MyComp; } TestBed.configureTestingModule( @@ -751,8 +777,8 @@ describe('di', () => { @Component({template: '
'}) class MyApp { showing = false; - @ViewChild(DirectiveA) dirA !: DirectiveA; - @ViewChild(DirectiveB) dirB !: DirectiveB; + @ViewChild(DirectiveA, {static: false}) dirA !: DirectiveA; + @ViewChild(DirectiveB, {static: false}) dirB !: DirectiveB; } TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyApp]}); @@ -858,6 +884,40 @@ describe('di', () => { const divElement = fixture.nativeElement.querySelector('div'); expect(divElement.textContent).toEqual('MyService'); }); + + it('should support sub-classes with no @Injectable decorator', () => { + @Injectable() + class Dependency { + } + + @Injectable() + class SuperClass { + constructor(public dep: Dependency) {} + } + + // Note, no @Injectable decorators for these two classes + class SubClass extends SuperClass {} + class SubSubClass extends SubClass {} + + @Component({template: ''}) + class MyComp { + constructor(public myService: SuperClass) {} + } + TestBed.configureTestingModule({ + declarations: [MyComp], + providers: [{provide: SuperClass, useClass: SubSubClass}, Dependency] + }); + + const warnSpy = spyOn(console, 'warn'); + const fixture = TestBed.createComponent(MyComp); + expect(fixture.componentInstance.myService.dep instanceof Dependency).toBe(true); + + if (ivyEnabled) { + expect(warnSpy).toHaveBeenCalledWith( + `DEPRECATED: DI is instantiating a token "SubSubClass" that inherits its @Injectable decorator but does not provide one itself.\n` + + `This will become an error in v10. Please add @Injectable() to the "SubSubClass" class.`); + } + }); }); describe('inject', () => { @@ -918,8 +978,8 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(InjectorDir) injectorDir !: InjectorDir; - @ViewChild(OtherInjectorDir) otherInjectorDir !: OtherInjectorDir; + @ViewChild(InjectorDir, {static: false}) injectorDir !: InjectorDir; + @ViewChild(OtherInjectorDir, {static: false}) otherInjectorDir !: OtherInjectorDir; } TestBed.configureTestingModule({declarations: [InjectorDir, OtherInjectorDir, MyComp]}); @@ -944,7 +1004,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(InjectorDir) injectorDir !: InjectorDir; + @ViewChild(InjectorDir, {static: false}) injectorDir !: InjectorDir; } TestBed.configureTestingModule({declarations: [InjectorDir, MyComp]}); @@ -981,8 +1041,8 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyDir, MyOtherDir, MyComp]}); @@ -1013,7 +1073,7 @@ describe('di', () => { @Component({template: ''}) class MyComp { - @ViewChild(MyDir) directive !: MyDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1051,8 +1111,8 @@ describe('di', () => { template: '' }) class MyComp { - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyDir, MyOtherDir, MyComp]}); @@ -1095,7 +1155,7 @@ describe('di', () => { } @Component({template: '
'}) class MyComp { - @ViewChild(OptionalDir) directive !: OptionalDir; + @ViewChild(OptionalDir, {static: false}) directive !: OptionalDir; } TestBed.configureTestingModule({declarations: [OptionalDir, MyComp]}); @@ -1124,8 +1184,8 @@ describe('di', () => { } @Component({template: '
'}) class MyComp { - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyDir, MyOtherDir, MyComp]}); @@ -1190,9 +1250,9 @@ describe('di', () => { () => { @Component({selector: 'my-app', template: ''}) class MyApp { - @ViewChild(MyComp) component !: MyComp; - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyComp, {static: false}) component !: MyComp; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyApp, MyComp, MyDir, MyOtherDir]}); const fixture = TestBed.createComponent(MyApp); @@ -1213,8 +1273,8 @@ describe('di', () => { @Component({selector: 'my-comp', template: '
'}) class MyComp { constructor(public cdr: ChangeDetectorRef) {} - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyComp, MyDir, MyOtherDir]}); const fixture = TestBed.createComponent(MyComp); @@ -1240,9 +1300,9 @@ describe('di', () => { }) class MyApp { constructor(public cdr: ChangeDetectorRef) {} - @ViewChild(MyComp) component !: MyComp; - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyComp, {static: false}) component !: MyComp; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyApp, MyComp, MyDir, MyOtherDir]}); const fixture = TestBed.createComponent(MyApp); @@ -1268,8 +1328,8 @@ describe('di', () => { class MyComp { showing = true; constructor(public cdr: ChangeDetectorRef) {} - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyComp, MyDir, MyOtherDir]}); @@ -1291,8 +1351,8 @@ describe('di', () => { class MyComp { showing = true; constructor(public cdr: ChangeDetectorRef) {} - @ViewChild(MyDir) directive !: MyDir; - @ViewChild(MyOtherDir) otherDirective !: MyOtherDir; + @ViewChild(MyDir, {static: false}) directive !: MyDir; + @ViewChild(MyOtherDir, {static: false}) otherDirective !: MyOtherDir; } TestBed.configureTestingModule({declarations: [MyComp, MyDir, MyOtherDir]}); @@ -1341,7 +1401,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(InjectorDir) injectorDirInstance !: InjectorDir; + @ViewChild(InjectorDir, {static: false}) injectorDirInstance !: InjectorDir; } TestBed.configureTestingModule({declarations: [InjectorDir, MyComp]}); @@ -1434,7 +1494,7 @@ describe('di', () => { providers: [{provide: LOCALE_ID, useValue: 'en-GB'}] }) class MyComp { - @ViewChild(MyDir) myDir !: MyDir; + @ViewChild(MyDir, {static: false}) myDir !: MyDir; constructor(@Inject(LOCALE_ID) public localeId: string) {} } @@ -1456,7 +1516,7 @@ describe('di', () => { @Component({template: '
'}) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1480,7 +1540,7 @@ describe('di', () => { @Component( {template: ''}) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1505,7 +1565,7 @@ describe('di', () => { template: '' }) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1532,7 +1592,7 @@ describe('di', () => { '
' }) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1559,7 +1619,7 @@ describe('di', () => { template: '
' }) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); @@ -1590,7 +1650,7 @@ describe('di', () => { '
' }) class MyComp { - @ViewChild(MyDir) directiveInstance !: MyDir; + @ViewChild(MyDir, {static: false}) directiveInstance !: MyDir; } TestBed.configureTestingModule({declarations: [MyDir, MyComp]}); diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts index 6764573e7b..e6d7dde5c7 100644 --- a/packages/core/test/acceptance/directive_spec.ts +++ b/packages/core/test/acceptance/directive_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {Input} from '@angular/core/src/metadata'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -16,6 +18,7 @@ describe('directives', () => { @Directive({selector: 'ng-template[test]'}) class TestDirective { + constructor(public templateRef: TemplateRef) {} } @Directive({selector: '[title]'}) @@ -26,6 +29,77 @@ describe('directives', () => { class TestComponent { } + it('should match directives with attribute selectors on bindings', () => { + @Directive({selector: '[test]'}) + class TestDir { + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + + it('should not accidentally set inputs from attributes extracted from bindings / outputs', + () => { + @Directive({selector: '[test]'}) + class TestDir { + @Input() prop1: boolean|undefined; + @Input() prop2: boolean|undefined; + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate( + TestComponent, + ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.hasAttribute('prop1')).toBe(false); + expect(spanEl.hasAttribute('prop2')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + it('should match directives on ng-template', () => { TestBed.configureTestingModule({declarations: [TestComponent, TestDirective]}); TestBed.overrideTemplate(TestComponent, ``); @@ -34,6 +108,8 @@ describe('directives', () => { const nodesWithDirective = fixture.debugElement.queryAllNodes(By.directive(TestDirective)); expect(nodesWithDirective.length).toBe(1); + expect(nodesWithDirective[0].injector.get(TestDirective).templateRef instanceof TemplateRef) + .toBe(true); }); it('should match directives on ng-template created by * syntax', () => { @@ -46,6 +122,32 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(1); }); + it('should match directives on ', () => { + @Directive({selector: 'ng-container[directiveA]'}) + class DirectiveA { + constructor(public viewContainerRef: ViewContainerRef) {} + } + + @Component({ + selector: 'my-component', + template: ` + + Some content + ` + }) + class MyComponent { + visible = true; + } + + TestBed.configureTestingModule( + {declarations: [MyComponent, DirectiveA], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + const directiveA = fixture.debugElement.query(By.css('span')).injector.get(DirectiveA); + + expect(directiveA.viewContainerRef).toBeTruthy(); + }); + it('should match directives on i18n-annotated attributes', () => { TestBed.configureTestingModule({declarations: [TestComponent, TitleDirective]}); TestBed.overrideTemplate(TestComponent, ` @@ -82,6 +184,316 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(0); }); + it('should match directives with attribute selectors on outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const spanEl = fixture.nativeElement.children[0]; + + // "out" should not be part of reflected attributes + expect(spanEl.hasAttribute('out')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('span'); + expect(fixture.debugElement.query(By.directive(TestDir))).toBeTruthy(); + }); + }); + describe('outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + it('should allow outputs of directive on ng-template', () => { + @Component({template: ``}) + class TestComp { + @ViewChild(TestDir, {static: true}) testDir: TestDir|undefined; + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.testDir).toBeTruthy(); + expect(fixture.componentInstance.value).toBe(false); + + fixture.componentInstance.testDir !.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBe(true); + }); + + it('should allow outputs of directive on ng-container', () => { + @Component({ + template: ` + + Hello + ` + }) + class TestComp { + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + const testDir = fixture.debugElement.query(By.css('span')).injector.get(TestDir); + + expect(fixture.componentInstance.value).toBe(false); + + testDir.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeTruthy(); + }); + + }); + + describe('attribute shadowing behaviors', () => { + /** + * To match ViewEngine, we need to ensure the following behaviors + */ + + @Directive({ + selector: '[dir-with-title]', + }) + class DirWithTitle { + @Input() + title = ''; + } + + it('should set both the div attribute and the directive input for `title="value"`', () => { + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('title')).toBe('a'); + }); + + it('should set the directive input only, shadowing the title property of the div, for `[title]="value"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + // We are checking the property here, not the attribute, because in the case of + // [key]="value" we are always setting the property of the instance, and actually setting + // the attribute is just a side-effect of the DOM implementation. + expect(dirWithTitle.title).toBe('a'); + expect(div.title).toBe(''); + }); + + it('should allow setting directive `title` input with `[title]="value"` and a "attr.title" attribute with `attr.title="test"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('attr.title')).toBe('test'); + expect(div.title).toBe(''); + }); + + it('should allow setting directive `title` input with `[title]="value1"` and attribute with `[attr.title]="value2"`', + () => { + @Component({template: `
`}) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('title')).toBe('b'); + }); + + it('should allow setting directive `title` input with `[title]="value1"` and attribute with `attr.title="{{value2}}"`', + () => { + @Component( + {template: `
`}) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('title')).toBe('b'); + }); + + it('should allow setting directive `title` input with `title="{{value}}"` and a "attr.title" attribute with `attr.title="test"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('attr.title')).toBe('test'); + expect(div.title).toBe(''); + }); + + it('should allow setting directive `title` input with `title="{{value1}}"` and attribute with `[attr.title]="value2"`', + () => { + @Component( + {template: `
`}) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('title')).toBe('b'); + }); + + it('should allow setting directive `title` input with `title="{{value1}}"` and attribute with `attr.title="{{value2}}"`', + () => { + @Component( + {template: `
`}) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.getAttribute('title')).toBe('b'); + }); + + it('should set the directive input only, shadowing the title property on the div, for `title="{{value}}"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe('a'); + expect(div.title).toBe(''); + }); + + it('should set the title attribute only, not directive input, for `attr.title="{{value}}"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe(''); + expect(div.getAttribute('title')).toBe('a'); + }); + + it('should set the title attribute only, not directive input, for `[attr.title]="value"`', + () => { + @Component({template: `
`}) + class App { + value = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, DirWithTitle], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const dirWithTitle = + fixture.debugElement.query(By.directive(DirWithTitle)).injector.get(DirWithTitle); + const div = fixture.nativeElement.querySelector('div'); + expect(dirWithTitle.title).toBe(''); + expect(div.getAttribute('title')).toBe('a'); + }); + }); }); diff --git a/packages/core/test/render3/discovery_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts similarity index 51% rename from packages/core/test/render3/discovery_utils_spec.ts rename to packages/core/test/acceptance/discover_utils_spec.ts index ac6acb306d..6c13d569f0 100644 --- a/packages/core/test/render3/discovery_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -5,16 +5,15 @@ * 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 {StaticInjector} from '../../src/di/injector'; -import {createInjector} from '../../src/di/r3_injector'; -import {AttributeMarker, RenderFlags, getHostElement, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵi18n, ɵɵi18nApply, ɵɵi18nExp, ɵɵselect} from '../../src/render3/index'; -import {markDirty, ɵɵbind, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵlistener, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, InjectionToken, ViewChild} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {onlyInIvy} from '@angular/private/testing'; + +import {getHostElement, markDirty} from '../../src/render3/index'; import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent, loadLContext} from '../../src/render3/util/discovery_utils'; -import {NgIf} from './common_with_def'; -import {ComponentFixture} from './render_util'; - -describe('discovery utils', () => { +onlyInIvy('Ivy-specific utilities').describe('discovery utils', () => { let fixture: ComponentFixture; let myApp: MyApp; let dirA: DirectiveA[]; @@ -29,115 +28,46 @@ describe('discovery utils', () => { log = []; dirA = []; childComponent = []; - fixture = new ComponentFixture( - MyApp, {injector: createInjector(null, null, [{provide: String, useValue: 'Module'}])}); - child = fixture.hostElement.querySelectorAll('child'); - span = fixture.hostElement.querySelectorAll('span'); - div = fixture.hostElement.querySelectorAll('div'); - p = fixture.hostElement.querySelectorAll('p'); + TestBed.configureTestingModule({ + imports: [CommonModule], + declarations: [MyApp, DirectiveA, Child], + providers: [{provide: String, useValue: 'Module'}] + }); + fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + child = fixture.nativeElement.querySelectorAll('child'); + span = fixture.nativeElement.querySelectorAll('span'); + div = fixture.nativeElement.querySelectorAll('div'); + p = fixture.nativeElement.querySelectorAll('p'); }); - /** - * For all tests assume this set up - * - * ``` - * - * <#VIEW> - * {{text}} - *
- * - * <#VIEW> - *

- * - *
- * - * <#VIEW> - *

- * - *
- * - * <#VIEW> - *

- * - *
- * ICU expression - * - *
- * ``` - */ + @Component( + {selector: 'child', template: '

', providers: [{provide: String, useValue: 'Child'}]}) class Child { constructor() { childComponent.push(this); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Child) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'p'); - } - }, - features: [ɵɵProvidersFeature([{provide: String, useValue: 'Child'}])] - }); } + @Directive({selector: '[dirA]', exportAs: 'dirA'}) class DirectiveA { constructor() { dirA.push(this); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['', 'dirA', '']], - exportAs: ['dirA'], - factory: () => new DirectiveA(), - }); } - const MSG_DIV = `{�0�, select, - other {ICU expression} - }`; - + @Component({ + selector: 'my-app', + template: ` + {{text}} +
+ + + +

+ ` + }) class MyApp { text: string = 'INIT'; constructor() { myApp = this; } - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 13, - vars: 1, - directives: [Child, DirectiveA, NgIf], - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵlistener('click', $event => log.push($event)); - ɵɵtext(1); - ɵɵelementEnd(); - ɵɵelement(2, 'div', ['dirA', ''], ['div', '', 'foo', 'dirA']); - ɵɵelement(5, 'child'); - ɵɵelement(6, 'child', ['dirA', ''], ['child', '']); - ɵɵtemplate(8, function(rf: RenderFlags, ctx: never) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child'); - } - }, 1, 0, 'child', ['dirA', AttributeMarker.Template, 'ngIf']); - ɵɵelementStart(9, 'i18n'); - ɵɵi18n(10, MSG_DIV); - ɵɵelementEnd(); - ɵɵelementContainerStart(11); - { ɵɵtext(12, 'content'); } - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.text)); - ɵɵelementProperty(8, 'ngIf', ɵɵbind(true)); - ɵɵi18nExp(ɵɵbind(ctx.text)); - ɵɵi18nApply(10); - } - } - }); + log(event: any) { log.push(event); } } describe('getComponent', () => { @@ -151,7 +81,7 @@ describe('discovery utils', () => { expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/); }); it('should return component from element', () => { - expect(getComponent(fixture.hostElement)).toEqual(myApp); + expect(getComponent(fixture.nativeElement)).toEqual(myApp); expect(getComponent(child[0])).toEqual(childComponent[0]); expect(getComponent(child[1])).toEqual(childComponent[1]); }); @@ -171,7 +101,7 @@ describe('discovery utils', () => { describe('getHostElement', () => { it('should return element on component', () => { - expect(getHostElement(myApp)).toEqual(fixture.hostElement); + expect(getHostElement(myApp)).toEqual(fixture.nativeElement); expect(getHostElement(childComponent[0])).toEqual(child[0]); expect(getHostElement(childComponent[1])).toEqual(child[1]); }); @@ -186,7 +116,7 @@ describe('discovery utils', () => { describe('getInjector', () => { it('should return node-injector from element', () => { - expect(getInjector(fixture.hostElement).get(String)).toEqual('Module'); + expect(getInjector(fixture.nativeElement).get(String)).toEqual('Module'); expect(getInjector(child[0]).get(String)).toEqual('Child'); expect(getInjector(p[0]).get(String)).toEqual('Child'); }); @@ -203,7 +133,7 @@ describe('discovery utils', () => { describe('getDirectives', () => { it('should return empty array if no directives', () => { - expect(getDirectives(fixture.hostElement)).toEqual([]); + expect(getDirectives(fixture.nativeElement)).toEqual([]); expect(getDirectives(span[0])).toEqual([]); expect(getDirectives(child[0])).toEqual([]); }); @@ -215,7 +145,7 @@ describe('discovery utils', () => { describe('getViewComponent', () => { it('should return null when called on root component', () => { - expect(getViewComponent(fixture.hostElement)).toEqual(null); + expect(getViewComponent(fixture.nativeElement)).toEqual(null); expect(getViewComponent(myApp)).toEqual(null); }); it('should return containing component of child component', () => { @@ -242,7 +172,7 @@ describe('discovery utils', () => { describe('getLocalRefs', () => { it('should retrieve empty map', () => { - expect(getLocalRefs(fixture.hostElement)).toEqual({}); + expect(getLocalRefs(fixture.nativeElement)).toEqual({}); expect(getLocalRefs(myApp)).toEqual({}); expect(getLocalRefs(span[0])).toEqual({}); expect(getLocalRefs(child[0])).toEqual({}); @@ -274,7 +204,7 @@ describe('discovery utils', () => { describe('getListeners', () => { it('should return no listeners', () => { - expect(getListeners(fixture.hostElement)).toEqual([]); + expect(getListeners(fixture.nativeElement)).toEqual([]); expect(getListeners(child[0])).toEqual([]); }); it('should return the listeners', () => { @@ -290,7 +220,7 @@ describe('discovery utils', () => { describe('getInjectionTokens', () => { it('should retrieve tokens', () => { - expect(getInjectionTokens(fixture.hostElement)).toEqual([MyApp]); + expect(getInjectionTokens(fixture.nativeElement)).toEqual([MyApp]); expect(getInjectionTokens(child[0])).toEqual([String, Child]); expect(getInjectionTokens(child[1])).toEqual([String, Child, DirectiveA]); }); @@ -301,7 +231,7 @@ describe('discovery utils', () => { expect(span[0].textContent).toEqual('INIT'); myApp.text = 'WORKS'; markDirty(myApp); - fixture.requestAnimationFrame.flush(); + fixture.detectChanges(); expect(span[0].textContent).toEqual('WORKS'); }); }); @@ -314,23 +244,15 @@ describe('discovery utils', () => { }); it('should work on templates', () => { - const templateComment = Array.from(fixture.hostElement.childNodes) + const templateComment = Array.from(fixture.nativeElement.childNodes) .find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE) !; const lContext = loadLContext(templateComment); expect(lContext).toBeDefined(); expect(lContext.native as any).toBe(templateComment); }); - it('should work on ICU expressions', () => { - const icuComment = Array.from(fixture.hostElement.querySelector('i18n') !.childNodes) - .find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE) !; - const lContext = loadLContext(icuComment); - expect(lContext).toBeDefined(); - expect(lContext.native as any).toBe(icuComment); - }); - it('should work on ng-container', () => { - const ngContainerComment = Array.from(fixture.hostElement.childNodes) + const ngContainerComment = Array.from(fixture.nativeElement.childNodes) .find( (node: ChildNode) => node.nodeType === Node.COMMENT_NODE && node.textContent === `ng-container`) !; @@ -341,50 +263,26 @@ describe('discovery utils', () => { }); }); -describe('discovery utils deprecated', () => { - +onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => { describe('getRootComponents()', () => { it('should return a list of the root components of the application from an element', () => { - let innerComp: InnerComp; + @Component({selector: 'inner-comp', template: '
'}) class InnerComp { - static ngComponentDef = ɵɵdefineComponent({ - type: InnerComp, - selectors: [['inner-comp']], - factory: () => innerComp = new InnerComp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: InnerComp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div'); - } - } - }); } + @Component({selector: 'comp', template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'inner-comp'); - } - }, - directives: [InnerComp] - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, InnerComp]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const hostElm = fixture.hostElement; + const hostElm = fixture.nativeElement; const innerElm = hostElm.querySelector('inner-comp') !; const divElm = hostElm.querySelector('div') !; - const component = fixture.component; + const component = fixture.componentInstance; expect(getRootComponents(hostElm) !).toEqual([component]); expect(getRootComponents(innerElm) !).toEqual([component]); @@ -394,65 +292,46 @@ describe('discovery utils deprecated', () => { describe('getDirectives()', () => { it('should return a list of the directives that are on the given element', () => { - let myDir1Instance: MyDir1|null = null; - let myDir2Instance: MyDir2|null = null; - let myDir3Instance: MyDir2|null = null; - + @Directive({selector: '[my-dir-1]'}) class MyDir1 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir1, - selectors: [['', 'my-dir-1', '']], - factory: () => myDir1Instance = new MyDir1() - }); } + @Directive({selector: '[my-dir-2]'}) class MyDir2 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir2, - selectors: [['', 'my-dir-2', '']], - factory: () => myDir2Instance = new MyDir2() - }); } + @Directive({selector: '[my-dir-3]'}) class MyDir3 { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir3, - selectors: [['', 'my-dir-3', '']], - factory: () => myDir3Instance = new MyDir2() - }); } + @Component({ + selector: 'comp', + template: ` +
+
+ ` + }) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['my-dir-1', '', 'my-dir-2', '']); - ɵɵelement(1, 'div', ['my-dir-3']); - } - }, - directives: [MyDir1, MyDir2, MyDir3] - }); + @ViewChild(MyDir1, {static: false}) myDir1Instance !: MyDir1; + @ViewChild(MyDir2, {static: false}) myDir2Instance !: MyDir2; + @ViewChild(MyDir3, {static: false}) myDir3Instance !: MyDir3; } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, MyDir1, MyDir2, MyDir3]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const hostElm = fixture.hostElement; + const hostElm = fixture.nativeElement; const elements = hostElm.querySelectorAll('div'); const elm1 = elements[0]; const elm1Dirs = getDirectives(elm1); - expect(elm1Dirs).toContain(myDir1Instance !); - expect(elm1Dirs).toContain(myDir2Instance !); + expect(elm1Dirs).toContain(fixture.componentInstance.myDir1Instance !); + expect(elm1Dirs).toContain(fixture.componentInstance.myDir2Instance !); const elm2 = elements[1]; const elm2Dirs = getDirectives(elm2); - expect(elm2Dirs).toContain(myDir3Instance !); + expect(elm2Dirs).toContain(fixture.componentInstance.myDir3Instance !); }); }); @@ -460,81 +339,48 @@ describe('discovery utils deprecated', () => { it('should return an injector that can return directive instances', () => { + @Component({template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => {} - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); - - const nodeInjector = getInjector(fixture.hostElement); + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const nodeInjector = getInjector(fixture.nativeElement); expect(nodeInjector.get(Comp)).toEqual(jasmine.any(Comp)); }); it('should return an injector that falls-back to a module injector', () => { - + @Component({template: ''}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => {} - }); } class TestToken {} + const token = new InjectionToken('test token'); - const staticInjector = new StaticInjector([{provide: TestToken, useValue: new TestToken()}]); - const fixture = new ComponentFixture(Comp, {injector: staticInjector}); - fixture.update(); - - const nodeInjector = getInjector(fixture.hostElement); - expect(nodeInjector.get(TestToken)).toEqual(jasmine.any(TestToken)); + TestBed.configureTestingModule( + {declarations: [Comp], providers: [{provide: token, useValue: new TestToken()}]}); + const fixture = TestBed.createComponent(Comp); + const nodeInjector = getInjector(fixture.nativeElement); + expect(nodeInjector.get(token)).toEqual(jasmine.any(TestToken)); }); }); describe('getLocalRefs', () => { it('should return a map of local refs for an element', () => { + @Directive({selector: '[myDir]', exportAs: 'myDir'}) class MyDir { - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir, - selectors: [['', 'myDir', '']], - exportAs: ['myDir'], - factory: () => new MyDir() - }); } + @Component({template: '
'}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - //
- ɵɵelement(0, 'div', ['myDir'], ['elRef', '', 'dirRef', 'myDir']); - } - }, - directives: [MyDir] - }); } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp, MyDir]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const divEl = fixture.hostElement.querySelector('div') !; + const divEl = fixture.nativeElement.querySelector('div') !; const localRefs = getLocalRefs(divEl); expect(localRefs.elRef.tagName.toLowerCase()).toBe('div'); @@ -542,31 +388,16 @@ describe('discovery utils deprecated', () => { }); it('should return a map of local refs for an element with styling context', () => { + @Component({template: '
'}) class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, ctx: Comp) => { - if (rf & RenderFlags.Create) { - //
- ɵɵelementStart(0, 'div', null, ['elRef', '']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstylingApply(); - } - } - }); + color = 'red'; } - const fixture = new ComponentFixture(Comp); - fixture.update(); + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + fixture.detectChanges(); - const divEl = fixture.hostElement.querySelector('div') !; + const divEl = fixture.nativeElement.querySelector('div') !; const localRefs = getLocalRefs(divEl); expect(localRefs.elRef.tagName.toLowerCase()).toBe('div'); diff --git a/packages/core/test/acceptance/host_binding_spec.ts b/packages/core/test/acceptance/host_binding_spec.ts index cb61683447..4e0d3e17b0 100644 --- a/packages/core/test/acceptance/host_binding_spec.ts +++ b/packages/core/test/acceptance/host_binding_spec.ts @@ -121,7 +121,7 @@ describe('host bindings', () => { class ParentCmp { private _prop = ''; - @ViewChild('template', {read: ViewContainerRef}) + @ViewChild('template', {read: ViewContainerRef, static: false}) vcr: ViewContainerRef = null !; private child: ComponentRef = null !; @@ -314,7 +314,7 @@ describe('host bindings', () => { @Component({template: ''}) class App { - @ViewChild(Dir) directiveInstance !: Dir; + @ViewChild(Dir, {static: false}) directiveInstance !: Dir; } TestBed.configureTestingModule({declarations: [App, Dir]}); @@ -403,7 +403,7 @@ describe('host bindings', () => { ` }) class App { - @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + @ViewChild(HostBindingDir, {static: false}) hostBindingDir !: HostBindingDir; } TestBed.configureTestingModule({declarations: [App, SomeDir, HostTitleComp, HostBindingDir]}); @@ -471,7 +471,7 @@ describe('host bindings', () => { @Component({template: '
'}) class App { - @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + @ViewChild(HostBindingDir, {static: false}) hostBindingDir !: HostBindingDir; } TestBed.configureTestingModule({declarations: [App, SomeDir, HostBindingDir]}); @@ -539,7 +539,7 @@ describe('host bindings', () => { @Component({template: ''}) class App { - @ViewChild(HostBindingInputDir) hostBindingInputDir !: HostBindingInputDir; + @ViewChild(HostBindingInputDir, {static: false}) hostBindingInputDir !: HostBindingInputDir; isDisabled = true; } @@ -629,7 +629,7 @@ describe('host bindings', () => { ` }) class App { - @ViewChild(NameComp) nameComp !: NameComp; + @ViewChild(NameComp, {static: false}) nameComp !: NameComp; name = ''; } @@ -685,8 +685,8 @@ describe('host bindings', () => { ` }) class App { - @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; - @ViewChild(NameComp) nameComp !: NameComp; + @ViewChild(HostBindingComp, {static: false}) hostBindingComp !: HostBindingComp; + @ViewChild(NameComp, {static: false}) nameComp !: NameComp; name = ''; otherName = ''; } @@ -760,8 +760,8 @@ describe('host bindings', () => { @Component({template: ''}) class App { - @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; - @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; + @ViewChild(HostBindingComp, {static: false}) hostBindingComp !: HostBindingComp; + @ViewChild(HostBindingDir, {static: false}) hostBindingDir !: HostBindingDir; } TestBed.configureTestingModule({declarations: [App, HostBindingComp, HostBindingDir]}); @@ -799,7 +799,7 @@ describe('host bindings', () => { @Component({template: `{{ name }}`}) class App { - @ViewChild(HostBindingComp) hostBindingComp !: HostBindingComp; + @ViewChild(HostBindingComp, {static: false}) hostBindingComp !: HostBindingComp; name = ''; } @@ -849,8 +849,8 @@ describe('host bindings', () => { ` }) class App { - @ViewChild(SubDirective) subDir !: SubDirective; - @ViewChild(SuperDirective) superDir !: SuperDirective; + @ViewChild(SubDirective, {static: false}) subDir !: SubDirective; + @ViewChild(SuperDirective, {static: false}) superDir !: SuperDirective; } TestBed.configureTestingModule({declarations: [App, SuperDirective, SubDirective]}); @@ -958,7 +958,7 @@ describe('host bindings', () => { @Component({template: ''}) class App { - @ViewChild(HostBindingToStyles) hostBindingDir !: HostBindingToStyles; + @ViewChild(HostBindingToStyles, {static: false}) hostBindingDir !: HostBindingToStyles; } TestBed.configureTestingModule({declarations: [App, HostBindingToStyles]}); @@ -987,7 +987,7 @@ describe('host bindings', () => { @Component({template: '
'}) class App { - @ViewChild(HostBindingToStyles) hostBindingDir !: HostBindingToStyles; + @ViewChild(HostBindingToStyles, {static: false}) hostBindingDir !: HostBindingToStyles; } TestBed.configureTestingModule({declarations: [App, HostBindingToStyles, ContainerDir]}); @@ -1037,7 +1037,7 @@ describe('host bindings', () => { @Component({template: `<${tag} unsafeUrlHostBindingDir>`}) class App { - @ViewChild(UnsafeDir) unsafeDir !: UnsafeDir; + @ViewChild(UnsafeDir, {static: false}) unsafeDir !: UnsafeDir; } TestBed.configureTestingModule({declarations: [App, UnsafeDir]}); diff --git a/packages/core/test/acceptance/i18n_spec.ts b/packages/core/test/acceptance/i18n_spec.ts new file mode 100644 index 0000000000..4e9bc759b5 --- /dev/null +++ b/packages/core/test/acceptance/i18n_spec.ts @@ -0,0 +1,1085 @@ +/** + * @license + * Copyright Google Inc. 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 {registerLocaleData} from '@angular/common'; +import localeRo from '@angular/common/locales/ro'; +import {Component, ContentChild, ContentChildren, Directive, HostBinding, Input, LOCALE_ID, QueryList, TemplateRef, Type, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; + + +onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => { + beforeEach(() => { + TestBed.configureTestingModule({declarations: [AppComp, DirectiveWithTplRef]}); + }); + + it('should translate text', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `
text
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
texte
`); + }); + + it('should support interpolations', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}!': 'Bonjour {$interpolation}!'}}); + const fixture = initWithTemplate(AppComp, `
Hello {{name}}!
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
Bonjour Angular!
`); + fixture.componentRef.instance.name = `John`; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
Bonjour John!
`); + }); + + it('should support interpolations with custom interpolation config', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = initWithTemplate(AppComp, `
Hello {% name %}
`); + + expect(fixture.nativeElement.innerHTML).toBe('
Bonjour Angular
'); + }); + + it('should support interpolations with complex expressions', () => { + ɵi18nConfigureLocalize({ + translations: + {'{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)'} + }); + const fixture = + initWithTemplate(AppComp, `
{{ name | uppercase }} - {{ obj?.a?.b }}
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - (fr)
`); + fixture.componentRef.instance.obj = {a: {b: 'value'}}; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
ANGULAR - value (fr)
`); + }); + + it('should support elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': + 'Bonjour {$startTagSpan}monde{$closeTagSpan} et {$startTagDiv}univers{$closeTagDiv}!' + } + }); + const fixture = initWithTemplate( + AppComp, `
Hello world and
universe
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour monde et
univers
!
`); + }); + + it('should support removing elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startBoldText}my{$closeBoldText}{$startTagSpan}world{$closeTagSpan}': + 'Bonjour {$startTagSpan}monde{$closeTagSpan}' + } + }); + const fixture = + initWithTemplate(AppComp, `
Hello myworld
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour monde
!
`); + }); + + it('should support moving elements', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Hello {$startTagSpan}world{$closeTagSpan} and {$startTagDiv}universe{$closeTagDiv}!': + 'Bonjour {$startTagDiv}univers{$closeTagDiv} et {$startTagSpan}monde{$closeTagSpan}!' + } + }); + const fixture = initWithTemplate( + AppComp, `
Hello world and
universe
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Bonjour
univers
et monde!
`); + }); + + it('should support template directives', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Content: {$startTagDiv}before{$startTagSpan}middle{$closeTagSpan}after{$closeTagDiv}!': + 'Contenu: {$startTagDiv}avant{$startTagSpan}milieu{$closeTagSpan}après{$closeTagDiv}!' + } + }); + const fixture = initWithTemplate( + AppComp, + `
Content:
beforemiddleafter
!
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Contenu:
avantmilieuaprès
!
`); + + fixture.componentRef.instance.visible = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
Contenu: !
`); + }); + + it('should support multiple i18n blocks', () => { + ɵi18nConfigureLocalize({ + translations: { + 'trad {$interpolation}': 'traduction {$interpolation}', + 'start {$interpolation} middle {$interpolation_1} end': + 'start {$interpolation_1} middle {$interpolation} end', + '{$startTagC}trad{$closeTagC}{$startTagD}{$closeTagD}{$startTagE}{$closeTagE}': + '{$startTagE}{$closeTagE}{$startTagC}traduction{$closeTagC}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ trad {{name}} + hello + + trad + + + +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction Angular hello traduction
`); + }); + + it('should support multiple sibling i18n blocks', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Section 1': 'Section un', + 'Section 2': 'Section deux', + 'Section 3': 'Section trois', + } + }); + const fixture = initWithTemplate(AppComp, ` +
+
Section 1
+
Section 2
+
Section 3
+
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
Section un
Section deux
Section trois
`); + }); + + it('should support multiple sibling i18n blocks inside of a template directive', () => { + ɵi18nConfigureLocalize({ + translations: { + 'Section 1': 'Section un', + 'Section 2': 'Section deux', + 'Section 3': 'Section trois', + } + }); + const fixture = initWithTemplate(AppComp, ` +
    +
  • Section 1
  • +
  • Section 2
  • +
  • Section 3
  • +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
  • Section un
  • Section deux
  • Section trois
  • Section un
  • Section deux
  • Section trois
  • Section un
  • Section deux
  • Section trois
`); + }); + + it('should properly escape quotes in content', () => { + ɵi18nConfigureLocalize({ + translations: { + '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"' + } + }); + const fixture = + initWithTemplate(AppComp, `
'Single quotes' and "Double quotes"
`); + + expect(fixture.nativeElement.innerHTML) + .toEqual('
\'Guillemets simples\' et "Guillemets doubles"
'); + }); + + it('should correctly bind to context in nested template', () => { + ɵi18nConfigureLocalize({translations: {'Item {$interpolation}': 'Article {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, ` +
+
Item {{ id }}
+
+ `); + + const element = fixture.nativeElement; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + expect(child).toHaveText(`Article ${i + 1}`); + } + }); + + it('should ignore i18n attributes on self-closing tags', () => { + const fixture = initWithTemplate(AppComp, ''); + expect(fixture.nativeElement.innerHTML).toBe(``); + }); + + it('should handle i18n attribute with directives', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, `
Hello {{ name }}
`); + expect(fixture.nativeElement.firstChild).toHaveText('Bonjour Angular'); + }); + + it('should work correctly with event listeners', () => { + ɵi18nConfigureLocalize({translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + + @Component( + {selector: 'app-comp', template: `
Hello {{ name }}
`}) + class ListenerComp { + name = `Angular`; + clicks = 0; + + onClick() { this.clicks++; } + } + + TestBed.configureTestingModule({declarations: [ListenerComp]}); + const fixture = TestBed.createComponent(ListenerComp); + fixture.detectChanges(); + + const element = fixture.nativeElement.firstChild; + const instance = fixture.componentInstance; + + expect(element).toHaveText('Bonjour Angular'); + expect(instance.clicks).toBe(0); + + element.click(); + expect(instance.clicks).toBe(1); + }); + + describe('ng-container and ng-template support', () => { + it('should support ng-container', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `text`); + expect(fixture.nativeElement.innerHTML).toEqual(`texte`); + }); + + it('should handle single translation message within ng-template', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, `Hello {{ name }}`); + + const element = fixture.nativeElement; + expect(element).toHaveText('Bonjour Angular'); + }); + + it('should be able to act as child elements inside i18n block (plain text content)', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': + '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello + + + Bye + +
+ `); + + const element = fixture.nativeElement.firstChild; + expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir'); + }); + + it('should be able to act as child elements inside i18n block (text + tags)', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': + '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello + + + Hello + +
+ `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + expect(spans[i]).toHaveText('Bonjour'); + } + }); + + it('should be able to handle deep nested levels with templates', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan} Hello - 1 {$closeTagSpan}{$startTagSpan_1} Hello - 2 {$startTagSpan_1} Hello - 3 {$startTagSpan_1} Hello - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Hello - 5 {$closeTagSpan}': + '{$startTagSpan} Bonjour - 1 {$closeTagSpan}{$startTagSpan_1} Bonjour - 2 {$startTagSpan_1} Bonjour - 3 {$startTagSpan_1} Bonjour - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Bonjour - 5 {$closeTagSpan}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + Hello - 1 + + + Hello - 2 + + Hello - 3 + + Hello - 4 + + + + + Hello - 5 + +
+ `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + expect(spans[i].innerHTML).toContain(`Bonjour - ${i + 1}`); + } + }); + + it('should handle self-closing tags as content', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': + '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}' + } + }); + const content = `My logo`; + const fixture = initWithTemplate(AppComp, ` + + ${content} + + + ${content} + + `); + + const element = fixture.nativeElement; + const spans = element.getElementsByTagName('span'); + for (let i = 0; i < spans.length; i++) { + const child = spans[i]; + expect(child).toHaveText('Mon logo'); + } + }); + }); + + describe('should support ICU expressions', () => { + it('with no root node', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = + initWithTemplate(AppComp, `{count, select, 10 {ten} 20 {twenty} other {other}}`); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('with no i18n tag', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = initWithTemplate( + AppComp, `
{count, select, 10 {ten} 20 {twenty} other {other}}
`); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('multiple', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } - {name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
aucun email! - (Angular)
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
4 emails - (Angular)
`); + + fixture.componentRef.instance.count = 0; + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
aucun email! - (John)
`); + }); + + it('with custom interpolation config', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} other {{$interpolation}}}': + '{VAR_SELECT, select, 10 {dix} other {{$interpolation}}}' + } + }); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = + initWithTemplate(AppComp, `
{count, select, 10 {ten} other {{% name %}}}
`); + + expect(fixture.nativeElement).toHaveText(`Angular`); + }); + + it('inside HTML elements', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} {$startTagSpan}emails{$closeTagSpan}}}', + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } - {name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
aucun email! - (Angular)
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
4 emails - (Angular)
`); + + fixture.componentRef.instance.count = 0; + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
aucun email! - (John)
`); + }); + + it('inside template directives', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{name, select, + other {({{name}})} + }
`); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
(Angular)
`); + + fixture.componentRef.instance.visible = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('inside ng-container', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, other {(name)}}': '{VAR_SELECT, select, other {({$interpolation})}}' + } + }); + const fixture = initWithTemplate(AppComp, `{name, select, + other {({{name}})} + }`); + expect(fixture.nativeElement.innerHTML).toEqual(`(Angular)`); + }); + + it('inside ', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': + '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autre}}' + } + }); + const fixture = initWithTemplate(AppComp, ` + + {count, select, 10 {ten} 20 {twenty} other {other}} + + `); + + const element = fixture.nativeElement; + expect(element).toHaveText('autre'); + }); + + it('nested', () => { + ɵi18nConfigureLocalize({ + translations: { + '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {cats} dog {dogs} other {animals}}!}}': + '{VAR_PLURAL, plural, =0 {zero} other {{$interpolation} {VAR_SELECT, select, cat {chats} dog {chients} other {animaux}}!}}' + } + }); + const fixture = initWithTemplate(AppComp, `
{count, plural, + =0 {zero} + other {{{count}} {name, select, + cat {cats} + dog {dogs} + other {animals} + }!} + }
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
zero
`); + + fixture.componentRef.instance.count = 4; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(`
4 animaux!
`); + }); + + it('should return the correct plural form for ICU expressions when using a specific locale', + () => { + registerLocaleData(localeRo); + TestBed.configureTestingModule({providers: [{provide: LOCALE_ID, useValue: 'ro'}]}); + // We could also use `TestBed.overrideProvider(LOCALE_ID, {useValue: 'ro'});` + const fixture = initWithTemplate(AppComp, ` + {count, plural, + =0 {no email} + =one {one email} + =few {a few emails} + =other {lots of emails} + }`); + + expect(fixture.nativeElement.innerHTML).toEqual('no email'); + + // Change detection cycle, no model changes + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('no email'); + + fixture.componentInstance.count = 3; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('a few emails'); + + fixture.componentInstance.count = 1; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('one email'); + + fixture.componentInstance.count = 10; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('a few emails'); + + fixture.componentInstance.count = 20; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('lots of emails'); + + fixture.componentInstance.count = 0; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('no email'); + }); + + it('projection', () => { + @Component({selector: 'child', template: '
'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` + { + value // i18n(ph = "blah"), + plural, + =1 {one} + other {at least {{value}} .} + }` + }) + class Parent { + value = 3; + } + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({translations: {}}); + + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toContain('at least'); + }); + + it('with empty values', () => { + const fixture = initWithTemplate(AppComp, `{count, select, 10 {} 20 {twenty} other {other}}`); + + const element = fixture.nativeElement; + expect(element).toHaveText('other'); + }); + }); + + describe('should support attributes', () => { + it('text', () => { + ɵi18nConfigureLocalize({translations: {'text': 'texte'}}); + const fixture = initWithTemplate(AppComp, `
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('interpolations', () => { + ɵi18nConfigureLocalize( + {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, `
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('with pipes', () => { + ɵi18nConfigureLocalize( + {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); + const fixture = initWithTemplate( + AppComp, `
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
`); + }); + + it('multiple attributes', () => { + ɵi18nConfigureLocalize( + {translations: {'hello {$interpolation}': 'bonjour {$interpolation}'}}); + const fixture = initWithTemplate( + AppComp, + ``); + expect(fixture.nativeElement.innerHTML) + .toEqual(``); + + fixture.componentRef.instance.name = 'John'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(``); + }); + + it('on removed elements', () => { + ɵi18nConfigureLocalize( + {translations: {'text': 'texte', '{$startTagSpan}content{$closeTagSpan}': 'contenu'}}); + const fixture = + initWithTemplate(AppComp, `
content
`); + expect(fixture.nativeElement.innerHTML).toEqual(`
contenu
`); + }); + + it('with custom interpolation config', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const interpolation = ['{%', '%}'] as[string, string]; + TestBed.overrideComponent(AppComp, {set: {interpolation}}); + const fixture = + initWithTemplate(AppComp, `
`); + + const element = fixture.nativeElement.firstChild; + expect(element.title).toBe('Bonjour Angular'); + }); + + it('in nested template', () => { + ɵi18nConfigureLocalize({translations: {'Item {$interpolation}': 'Article {$interpolation}'}}); + const fixture = initWithTemplate(AppComp, ` +
+
+
`); + + const element = fixture.nativeElement; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + expect((child as any).innerHTML).toBe(`
`); + } + }); + + it('should add i18n attributes on self-closing tags', () => { + ɵi18nConfigureLocalize( + {translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}'}}); + const fixture = + initWithTemplate(AppComp, ``); + + const element = fixture.nativeElement.firstChild; + expect(element.title).toBe('Bonjour Angular'); + }); + }); + + it('should work with directives and host bindings', () => { + let directiveInstances: ClsDir[] = []; + + @Directive({selector: '[test]'}) + class ClsDir { + @HostBinding('className') + klass = 'foo'; + + constructor() { directiveInstances.push(this); } + } + + @Component({ + selector: `my-app`, + template: ` +
+ trad: {exp1, plural, + =0 {no emails!} + =1 {one email} + other {{{exp1}} emails} + } +
` + }) + class MyApp { + exp1 = 1; + exp2 = 2; + } + + TestBed.configureTestingModule({declarations: [ClsDir, MyApp]}); + ɵi18nConfigureLocalize({ + translations: { + 'start {$interpolation} middle {$interpolation_1} end': + 'début {$interpolation_1} milieu {$interpolation} fin', + '{VAR_PLURAL, plural, =0 {no {$startBoldText}emails{$closeBoldText}!} =1 {one {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}': + '{VAR_PLURAL, plural, =0 {aucun {$startBoldText}email{$closeBoldText}!} =1 {un {$startItalicText}email{$closeItalicText}} other {{$interpolation} emails}}', + ' trad: {$icu}': ' traduction: {$icu}' + } + }); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction: un email
`); + + directiveInstances.forEach(instance => instance.klass = 'bar'); + fixture.componentRef.instance.exp1 = 2; + fixture.componentRef.instance.exp2 = 3; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
traduction: 2 emails
`); + }); + + it('should handle i18n attribute with directive inputs', () => { + let calledTitle = false; + let calledValue = false; + @Component({selector: 'my-comp', template: ''}) + class MyComp { + t !: string; + @Input() + get title() { return this.t; } + set title(title) { + calledTitle = true; + this.t = title; + } + + @Input() + get value() { return this.val; } + set value(value: string) { + calledValue = true; + this.val = value; + } + val !: string; + } + + TestBed.configureTestingModule({declarations: [AppComp, MyComp]}); + ɵi18nConfigureLocalize({ + translations: {'Hello {$interpolation}': 'Bonjour {$interpolation}', 'works': 'fonctionne'} + }); + const fixture = initWithTemplate( + AppComp, + ``); + fixture.detectChanges(); + + const directive = fixture.debugElement.children[0].injector.get(MyComp); + expect(calledValue).toEqual(true); + expect(calledTitle).toEqual(true); + expect(directive.value).toEqual(`Bonjour Angular`); + expect(directive.title).toEqual(`fonctionne`); + }); + + it('should support adding/moving/removing nodes', () => { + ɵi18nConfigureLocalize({ + translations: { + '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv3}{$closeTagDiv3}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}{$startTagDiv6}{$closeTagDiv6}{$startTagDiv7}{$closeTagDiv7}{$startTagDiv8}{$closeTagDiv8}': + '{$startTagDiv2}{$closeTagDiv2}{$startTagDiv8}{$closeTagDiv8}{$startTagDiv4}{$closeTagDiv4}{$startTagDiv5}{$closeTagDiv5}Bonjour monde{$startTagDiv3}{$closeTagDiv3}{$startTagDiv7}{$closeTagDiv7}' + } + }); + const fixture = initWithTemplate(AppComp, ` +
+ + + + + + + +
`); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `
Bonjour monde
`); + }); + + describe('projection', () => { + it('should project the translations', () => { + @Component({selector: 'child', template: '

'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` +
+ I am projected from + {{name}} + + + +
` + }) + class Parent { + name: string = 'Parent'; + } + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + 'Child of {$interpolation}': 'Enfant de {$interpolation}', + '{$startTagChild}I am projected from {$startBoldText}{$interpolation}{$startTagRemoveMe_1}{$closeTagRemoveMe_1}{$closeBoldText}{$startTagRemoveMe_2}{$closeTagRemoveMe_2}{$closeTagChild}{$startTagRemoveMe_3}{$closeTagRemoveMe_3}': + '{$startTagChild}Je suis projeté depuis {$startBoldText}{$interpolation}{$closeBoldText}{$closeTagChild}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent

`); + }); + + it('should project a translated i18n block', () => { + @Component({selector: 'child', template: '

'}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` +
+ + + I am projected from {{name}} + + +
` + }) + class Parent { + name: string = 'Parent'; + } + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + 'Child of {$interpolation}': 'Enfant de {$interpolation}', + 'I am projected from {$interpolation}': 'Je suis projeté depuis {$interpolation}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent

`); + + // it should be able to render a new component with the same template code + const fixture2 = TestBed.createComponent(Parent); + fixture2.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(fixture2.nativeElement.innerHTML); + + fixture2.componentRef.instance.name = 'Parent 2'; + fixture2.detectChanges(); + expect(fixture2.nativeElement.innerHTML) + .toEqual( + `

Je suis projeté depuis Parent 2

`); + + // The first fixture should not have changed + expect(fixture.nativeElement.innerHTML).not.toEqual(fixture2.nativeElement.innerHTML); + }); + + it('should re-project translations when multiple projections', () => { + @Component({selector: 'grand-child', template: '
'}) + class GrandChild { + } + + @Component( + {selector: 'child', template: ''}) + class Child { + } + + @Component({selector: 'parent', template: `Hello World!`}) + class Parent { + name: string = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startBoldText}Hello{$closeBoldText} World!': + '{$startBoldText}Bonjour{$closeBoldText} monde!' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('
Bonjour monde!
'); + }); + + // FW-1319 Runtime i18n should be able to remove projected placeholders + xit('should be able to remove projected placeholders', () => { + @Component({selector: 'grand-child', template: '
'}) + class GrandChild { + } + + @Component( + {selector: 'child', template: ''}) + class Child { + } + + @Component({selector: 'parent', template: `Hello World!`}) + class Parent { + name: string = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Parent, Child, GrandChild]}); + ɵi18nConfigureLocalize( + {translations: {'{$startBoldText}Hello{$closeBoldText} World!': 'Bonjour monde!'}}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('
Bonjour monde!
'); + }); + + it('should project translations with selectors', () => { + @Component({selector: 'child', template: ``}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` + + + + + ` + }) + class Parent { + } + + TestBed.configureTestingModule({declarations: [Parent, Child]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startTagSpan}{$closeTagSpan}{$startTagSpan_1}{$closeTagSpan}': + '{$startTagSpan}Contenu{$closeTagSpan}' + } + }); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual('Contenu'); + }); + }); + + describe('queries', () => { + function toHtml(element: Element): string { + return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '') + .replace(//g, ''); + } + + it('detached nodes should still be part of query', () => { + @Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'}) + class TextDirective { + // TODO(issue/24571): remove '!'. + text !: string; + constructor() {} + } + + @Component({selector: 'div-query', template: ''}) + class DivQuery { + // TODO(issue/24571): remove '!'. + @ContentChild(TemplateRef, {static: true}) template !: TemplateRef; + + // TODO(issue/24571): remove '!'. + @ViewChild('vc', {read: ViewContainerRef, static: true}) + vc !: ViewContainerRef; + + // TODO(issue/24571): remove '!'. + @ContentChildren(TextDirective, {descendants: true}) + query !: QueryList; + + create() { this.vc.createEmbeddedView(this.template); } + + destroy() { this.vc.clear(); } + } + + TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]}); + ɵi18nConfigureLocalize({ + translations: { + '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': + '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' + } + }); + const fixture = initWithTemplate(AppComp, ` + + +
+
+ Content +
+
+
+
+ `); + const q = fixture.debugElement.children[0].references.q; + expect(q.query.length).toEqual(0); + + // Create embedded view + q.create(); + fixture.detectChanges(); + expect(q.query.length).toEqual(1); + expect(toHtml(fixture.nativeElement)) + .toEqual(`Contenu`); + + // Disable ng-if + fixture.componentInstance.visible = false; + fixture.detectChanges(); + expect(q.query.length).toEqual(0); + expect(toHtml(fixture.nativeElement)) + .toEqual(`Contenu`); + }); + }); +}); + +function initWithTemplate(compType: Type, template: string) { + TestBed.overrideComponent(compType, {set: {template}}); + const fixture = TestBed.createComponent(compType); + fixture.detectChanges(); + return fixture; +} + +@Component({selector: 'app-comp', template: ``}) +class AppComp { + name = `Angular`; + visible = true; + count = 0; +} + +@Directive({ + selector: '[tplRef]', +}) +class DirectiveWithTplRef { + constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} + ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } +} diff --git a/packages/core/test/acceptance/inherit_definition_feature_spec.ts b/packages/core/test/acceptance/inherit_definition_feature_spec.ts index cc053dea37..682cde4e34 100644 --- a/packages/core/test/acceptance/inherit_definition_feature_spec.ts +++ b/packages/core/test/acceptance/inherit_definition_feature_spec.ts @@ -6,155 +6,4852 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Input, OnChanges, Type} from '@angular/core'; +import {Component, ContentChildren, Directive, EventEmitter, HostBinding, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {onlyInIvy} from '@angular/private/testing'; -describe('ngOnChanges', () => { - it('should be inherited when super is a directive', () => { - const log: string[] = []; +describe('inheritance', () => { + onlyInIvy('View Engine does not provide this check') + .it('should throw when trying to inherit a component from a directive', () => { + @Component({ + selector: 'my-comp', + template: '
', + }) + class MyComponent { + } - @Directive({selector: '[superDir]'}) - class SuperDirective implements OnChanges { - @Input() someInput = ''; + @Directive({ + selector: '[my-dir]', + }) + class MyDirective extends MyComponent { + } - ngOnChanges() { log.push('on changes!'); } - } + @Component({ + template: `
`, + }) + class App { + } - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperDirective { - } + TestBed.configureTestingModule({ + declarations: [App, MyComponent, MyDirective], + }); - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
'})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + expect(() => { + TestBed.createComponent(App); + }).toThrowError('Directives cannot inherit Components'); + }); - expect(log).toEqual(['on changes!']); + describe('ngOnChanges', () => { + it('should be inherited when super is a directive', () => { + const log: string[] = []; + + @Directive({selector: '[superDir]'}) + class SuperDirective implements OnChanges { + @Input() someInput = ''; + + ngOnChanges() { log.push('on changes!'); } + } + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperDirective { + } + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); + + it('should be inherited when super is a simple class', () => { + const log: string[] = []; + + class SuperClass { + ngOnChanges() { log.push('on changes!'); } + } + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperClass { + @Input() someInput = ''; + } + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); + + it('should be inherited when super is a directive and grand-super is a directive', () => { + const log: string[] = []; + + @Directive({selector: '[grandSuperDir]'}) + class GrandSuperDirective implements OnChanges { + @Input() someInput = ''; + + ngOnChanges() { log.push('on changes!'); } + } + + @Directive({selector: '[superDir]'}) + class SuperDirective extends GrandSuperDirective { + } + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperDirective { + } + + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective, GrandSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); + + it('should be inherited when super is a directive and grand-super is a simple class', () => { + const log: string[] = []; + + class GrandSuperClass { + ngOnChanges() { log.push('on changes!'); } + } + + @Directive({selector: '[superDir]'}) + class SuperDirective extends GrandSuperClass { + @Input() someInput = ''; + } + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperDirective { + } + + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); + + it('should be inherited when super is a simple class and grand-super is a directive', () => { + const log: string[] = []; + + @Directive({selector: '[grandSuperDir]'}) + class GrandSuperDirective implements OnChanges { + @Input() someInput = ''; + + ngOnChanges() { log.push('on changes!'); } + } + + class SuperClass extends GrandSuperDirective {} + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperClass { + } + + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, GrandSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); + + it('should be inherited when super is a simple class and grand-super is a simple class', () => { + const log: string[] = []; + + class GrandSuperClass { + ngOnChanges() { log.push('on changes!'); } + } + + class SuperClass extends GrandSuperClass {} + + @Directive({selector: '[subDir]'}) + class SubDirective extends SuperClass { + @Input() someInput = ''; + } + + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(log).toEqual(['on changes!']); + }); }); - it('should be inherited when super is a simple class', () => { - const log: string[] = []; + describe('of bare super class by a directive', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild - class SuperClass { - ngOnChanges() { log.push('on changes!'); } - } + describe('lifecycle hooks', () => { + const fired: string[] = []; - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperClass { - @Input() someInput = ''; - } + class SuperDirective { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
'})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + beforeEach(() => fired.length = 0); - expect(log).toEqual(['on changes!']); + it('ngOnInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { fired.push('sub init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + class SuperDirective { + @Input() + foo = ''; + + @Input() + bar = ''; + + @Input() + baz = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: `

`}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir = + fixture.debugElement.query(By.directive(SubDirective)).injector.get(SubDirective); + + expect(subDir.foo).toBe('a'); + expect(subDir.bar).toBe('b'); + expect(subDir.baz).toBe('c'); + expect(subDir.qux).toBe('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + class SuperDirective { + @Output() + foo = new EventEmitter(); + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` +
+ ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + class SuperDirective { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(SubDirective)); + + expect(queryResult.nativeElement.tagName).toBe('P'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + class SuperDirective { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(SubDirective)); + + expect(queryResult.nativeElement.title).toBe('test!!!'); + }); + }); + + describe('ContentChildren', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + class SuperDirective { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` +
    +
  • one
  • +
  • two
  • +
+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, ChildDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); }); - it('should be inherited when super is a directive and grand-super is a directive', () => { - const log: string[] = []; + describe('of a directive by another directive', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild - @Directive({selector: '[grandSuperDir]'}) - class GrandSuperDirective implements OnChanges { - @Input() someInput = ''; + describe('lifecycle hooks', () => { + const fired: string[] = []; - ngOnChanges() { log.push('on changes!'); } - } + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } - @Directive({selector: '[superDir]'}) - class SuperDirective extends GrandSuperDirective { - } + beforeEach(() => fired.length = 0); - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperDirective { - } + it('ngOnInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { fired.push('sub init'); } + } - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
'})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + @Component({ + template: `

`, + }) + class App { + showing = true; + } - expect(log).toEqual(['on changes!']); + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Directive({selector: '[super-dir]'}) + class SuperDirective { + @Input() + foo = ''; + + @Input() + bar = ''; + + @Input() + baz = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: `

`}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir = + fixture.debugElement.query(By.directive(SubDirective)).injector.get(SubDirective); + + expect(subDir.foo).toBe('a'); + expect(subDir.bar).toBe('b'); + expect(subDir.baz).toBe('c'); + expect(subDir.qux).toBe('d'); + }); + + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @Output() + foo = new EventEmitter(); + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` +
+ ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(SubDirective)); + + expect(queryResult.nativeElement.tagName).toBe('P'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(SubDirective)); + + expect(queryResult.nativeElement.title).toBe('test!!!'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` +
    +
  • one
  • +
  • two
  • +
+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, ChildDir, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); }); - it('should be inherited when super is a directive and grand-super is a simple class', () => { - const log: string[] = []; + describe('of a directive by a bare class then by another directive', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; - class GrandSuperClass { - ngOnChanges() { log.push('on changes!'); } - } + @Directive({ + selector: '[super-dir]', + }) + class SuperSuperDirective { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } - @Directive({selector: '[superDir]'}) - class SuperDirective extends GrandSuperClass { - @Input() someInput = ''; - } + class SuperDirective extends SuperSuperDirective {} - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperDirective { - } + beforeEach(() => fired.length = 0); - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
'})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + it('ngOnInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { fired.push('sub init'); } + } - expect(log).toEqual(['on changes!']); + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Directive({ + selector: '[subDir]', + }) + class SubDirective extends SuperDirective { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: `

`, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [SubDirective, App, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Directive({selector: '[super-dir]'}) + class SuperSuperDirective { + @Input() + foo = ''; + + @Input() + baz = ''; + } + + class SuperDirective extends SuperSuperDirective { + @Input() + bar = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({ + selector: 'my-app', + template: `

`, + }) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir = + fixture.debugElement.query(By.directive(SubDirective)).injector.get(SubDirective); + + expect(subDir.foo).toBe('a'); + expect(subDir.bar).toBe('b'); + expect(subDir.baz).toBe('c'); + expect(subDir.qux).toBe('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperSuperDirective { + @Output() + foo = new EventEmitter(); + } + + class SuperDirective extends SuperSuperDirective { + @Output() + bar = new EventEmitter(); + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngOnInit() { + this.foo.emit('test1'); + this.bar.emit('test2'); + } + } + + @Component({ + template: ` +
+ ` + }) + class App { + foo = ''; + + bar = ''; + + handleFoo(event: string) { this.foo = event; } + + handleBar(event: string) { this.bar = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test1'); + expect(app.bar).toBe('test2'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperSuperDirective { + @HostBinding('style.color') + color = 'red'; + } + + class SuperDirective extends SuperSuperDirective { + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(SubDirective)); + + expect(queryResult.nativeElement.tagName).toBe('P'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperSuperDirective { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + class SuperDirective extends SuperSuperDirective { + @HostBinding('accessKey') + get boundAltKey() { return this.superAccessKey + '???'; } + + @Input() + superAccessKey = ''; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + } + @Component({ + template: ` +

test

+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, SuperDirective, SuperSuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const p: HTMLParagraphElement = + fixture.debugElement.query(By.directive(SubDirective)).nativeElement; + + expect(p.title).toBe('test1!!!'); + expect(p.accessKey).toBe('test2???'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundChildDir1s: QueryList; + let foundChildDir2s: QueryList; + + @Directive({selector: '[child-dir-one]'}) + class ChildDir1 { + } + + @Directive({selector: '[child-dir-two]'}) + class ChildDir2 { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperSuperDirective { + @ContentChildren(ChildDir1) + childDir1s !: QueryList; + } + + class SuperDirective extends SuperSuperDirective { + @ContentChildren(ChildDir1) + childDir2s !: QueryList; + } + + @Directive({ + selector: '[sub-dir]', + }) + class SubDirective extends SuperDirective { + ngAfterViewInit() { + foundChildDir1s = this.childDir1s; + foundChildDir2s = this.childDir2s; + } + } + + @Component({ + template: ` +
    +
  • one
  • +
  • two
  • +
  • three
  • +
+ ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SubDirective, ChildDir1, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundChildDir1s !.length).toBe(2); + expect(foundChildDir2s !.length).toBe(2); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); }); - it('should be inherited when super is a simple class and grand-super is a directive', () => { - const log: string[] = []; + describe('of bare super class by a component', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; - @Directive({selector: '[grandSuperDir]'}) - class GrandSuperDirective implements OnChanges { - @Input() someInput = ''; + class SuperComponent { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } - ngOnChanges() { log.push('on changes!'); } - } + beforeEach(() => fired.length = 0); - class SuperClass extends GrandSuperDirective {} + it('ngOnInit', () => { + @Component({selector: 'my-comp', template: `

test

`}) + class MyComponent extends SuperComponent { + ngOnInit() { fired.push('sub init'); } + } - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperClass { - } + @Component({ + template: ``, + }) + class App { + showing = true; + } - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
'})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); - expect(log).toEqual(['on changes!']); + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Directive({ + selector: 'my-comp', + }) + class MyComponent extends SuperComponent { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + class SuperComponent { + @Input() + foo = ''; + + @Input() + bar = ''; + + @Input() + baz = ''; + } + + @Component({selector: 'my-comp', template: `

test

`}) + class MyComponent extends SuperComponent { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: ``}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir: MyComponent = + fixture.debugElement.query(By.directive(MyComponent)).componentInstance; + + expect(subDir.foo).toEqual('a'); + expect(subDir.bar).toEqual('b'); + expect(subDir.baz).toEqual('c'); + expect(subDir.qux).toEqual('d'); + }); + + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + class SuperComponent { + @Output() + foo = new EventEmitter(); + } + + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` + + ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + class SuperComponent { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + class SuperComponent { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + @Component({ + selector: 'my-comp', + template: `

test

`, + }) + class MyComponent extends SuperComponent { + } + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.title).toBe('test!!!'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + class SuperComponent { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({selector: 'my-comp', template: `
`}) + class MyComponent extends SuperComponent { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + +
  • one
  • +
  • two
  • +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); }); - it('should be inherited when super is a simple class and grand-super is a simple class', () => { - const log: string[] = []; + describe('of a directive inherited by a component', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; - class GrandSuperClass { - ngOnChanges() { log.push('on changes!'); } - } + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } - class SuperClass extends GrandSuperClass {} + beforeEach(() => fired.length = 0); - @Directive({selector: '[subDir]'}) - class SubDirective extends SuperClass { - @Input() someInput = ''; - } + it('ngOnInit', () => { + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends SuperDirective { + ngOnInit() { fired.push('sub init'); } + } - TestBed.configureTestingModule({declarations: [AppComp, SubDirective]}); - TestBed.overrideComponent( - AppComp, {set: new Component({template: '
    '})}); - const fixture = TestBed.createComponent(AppComp); - fixture.detectChanges(); + @Component({ + template: ``, + }) + class App { + showing = true; + } - expect(log).toEqual(['on changes!']); + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @Input() + foo = ''; + + @Input() + bar = ''; + + @Input() + baz = ''; + } + + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends SuperDirective { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: ``}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir: MyComponent = + fixture.debugElement.query(By.directive(MyComponent)).componentInstance; + + expect(subDir.foo).toEqual('a'); + expect(subDir.bar).toEqual('b'); + expect(subDir.baz).toEqual('c'); + expect(subDir.qux).toEqual('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @Output() + foo = new EventEmitter(); + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` + + ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperDirective { + } + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.title).toBe('test!!!'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({selector: 'my-comp', template: `
    `}) + class MyComponent extends SuperDirective { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + +
  • one
  • +
  • two
  • +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperDirective, ChildDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + it('should inherit ViewChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @ViewChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({ + selector: 'my-comp', + template: ` +
      +
    • {{item}}
    • +
    + `, + }) + class MyComponent extends SuperDirective { + items = [1, 2, 3, 4, 5]; + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(5); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); + }); + + describe('of a directive inherited by a bare class and then by a component', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } + + class BareClass extends SuperDirective {} + + beforeEach(() => fired.length = 0); + + it('ngOnInit', () => { + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends BareClass { + ngOnInit() { fired.push('sub init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Directive({ + selector: 'my-comp', + }) + class MyComponent extends BareClass { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @Input() + foo = ''; + + @Input() + baz = ''; + } + + class BareClass extends SuperDirective { + @Input() + bar = ''; + } + + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends BareClass { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: ``}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, BareClass, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir: MyComponent = + fixture.debugElement.query(By.directive(MyComponent)).componentInstance; + + expect(subDir.foo).toEqual('a'); + expect(subDir.bar).toEqual('b'); + expect(subDir.baz).toEqual('c'); + expect(subDir.qux).toEqual('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @Output() + foo = new EventEmitter(); + } + + class BareClass extends SuperDirective {} + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` + + ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + class BareClass extends SuperDirective {} + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + class BareClass extends SuperDirective { + @HostBinding('accessKey') + get boundAccessKey() { return this.superAccessKey + '???'; } + + @Input() + superAccessKey = ''; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends BareClass { + } + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, SuperDirective, BareClass, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.title).toBe('test1!!!'); + expect(queryResult.nativeElement.accessKey).toBe('test2???'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + class BareClass extends SuperDirective {} + + @Component({ + selector: 'my-comp', + template: `
    `, + }) + class MyComponent extends BareClass { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + +
  • one
  • +
  • two
  • +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + it('should inherit ViewChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Directive({ + selector: '[super-dir]', + }) + class SuperDirective { + @ViewChildren(ChildDir) + customDirs !: QueryList; + } + + class BareClass extends SuperDirective {} + + @Component({ + selector: 'my-comp', + template: ` +
      +
    • {{item}}
    • +
    + `, + }) + class MyComponent extends BareClass { + items = [1, 2, 3, 4, 5]; + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(5); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); + }); + + describe('of a component inherited by a component', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } + + beforeEach(() => fired.length = 0); + + it('ngOnInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnInit() { fired.push('sub init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @Input() + foo = ''; + + @Input() + bar = ''; + + @Input() + baz = ''; + } + + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends SuperComponent { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: ``}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const subDir: MyComponent = + fixture.debugElement.query(By.directive(MyComponent)).componentInstance; + + expect(subDir.foo).toEqual('a'); + expect(subDir.bar).toEqual('b'); + expect(subDir.baz).toEqual('c'); + expect(subDir.qux).toEqual('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @Output() + foo = new EventEmitter(); + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnInit() { this.foo.emit('test'); } + } + + @Component({ + template: ` + + ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @HostBinding('style.color') + color = 'red'; + + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + } + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.title).toBe('test!!!'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({ + selector: 'my-comp', + template: `
    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + +
  • one
  • +
  • two
  • +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent, ChildDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + it('should inherit ViewChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @ViewChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({ + selector: 'my-comp', + template: ` +
      +
    • {{item}}
    • +
    + `, + }) + class MyComponent extends SuperComponent { + items = [1, 2, 3, 4, 5]; + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(5); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); + }); + + describe('of a component inherited by a bare class then by a component', () => { + // TODO: Add tests for ContentChild + // TODO: Add tests for ViewChild + describe('lifecycle hooks', () => { + const fired: string[] = []; + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperSuperComponent { + ngOnInit() { fired.push('super init'); } + ngOnDestroy() { fired.push('super destroy'); } + ngAfterContentInit() { fired.push('super after content init'); } + ngAfterContentChecked() { fired.push('super after content checked'); } + ngAfterViewInit() { fired.push('super after view init'); } + ngAfterViewChecked() { fired.push('super after view checked'); } + ngDoCheck() { fired.push('super do check'); } + } + + class SuperComponent extends SuperSuperComponent {} + + beforeEach(() => fired.length = 0); + + it('ngOnInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnInit() { fired.push('sub init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngDoCheck', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngDoCheck() { fired.push('sub do check'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'sub do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterContentInit() { fired.push('sub after content init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'sub after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterContentChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterContentChecked() { fired.push('sub after content checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'sub after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewInit', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewInit() { fired.push('sub after view init'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'sub after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngAfterViewChecked', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewChecked() { fired.push('sub after view checked'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'sub after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super destroy', + ]); + }); + + it('ngOnDestroy', () => { + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnDestroy() { fired.push('sub destroy'); } + } + + @Component({ + template: ``, + }) + class App { + showing = true; + } + + TestBed.configureTestingModule({ + declarations: [MyComponent, App, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fired).toEqual([ + 'super init', + 'super do check', + 'super after content init', + 'super after content checked', + 'super after view init', + 'super after view checked', + ]); + + fired.length = 0; + fixture.componentInstance.showing = false; + fixture.detectChanges(); + + expect(fired).toEqual([ + 'sub destroy', + ]); + }); + }); + + describe('inputs', () => { + // TODO: add test where the two inputs have a different alias + // TODO: add test where super has an @Input() on the property, and sub does not + // TODO: add test where super has an @Input('alias') on the property and sub has no alias + + it('should inherit inputs', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperSuperComponent { + @Input() + foo = ''; + + @Input() + baz = ''; + } + + class BareClass extends SuperSuperComponent { + @Input() + bar = ''; + } + + @Component({selector: 'my-comp', template: `

    test

    `}) + class MyComponent extends BareClass { + @Input() + baz = ''; + + @Input() + qux = ''; + } + + @Component({template: ``}) + class App { + a = 'a'; + b = 'b'; + c = 'c'; + d = 'd'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperSuperComponent, BareClass], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const myComp: MyComponent = + fixture.debugElement.query(By.directive(MyComponent)).componentInstance; + + expect(myComp.foo).toEqual('a'); + expect(myComp.bar).toEqual('b'); + expect(myComp.baz).toEqual('c'); + expect(myComp.qux).toEqual('d'); + }); + }); + + describe('outputs', () => { + // TODO: add tests where both sub and super have Output on same property with different + // aliases + // TODO: add test where super has property with alias and sub has property with no alias + // TODO: add test where super has an @Input() on the property, and sub does not + + it('should inherit outputs', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperSuperComponent { + @Output() + foo = new EventEmitter(); + } + + class SuperComponent extends SuperSuperComponent { + @Output() + bar = new EventEmitter(); + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + ngOnInit() { + this.foo.emit('test1'); + this.bar.emit('test2'); + } + } + + @Component({ + template: ` + + ` + }) + class App { + foo = ''; + + handleFoo(event: string) { this.foo = event; } + + bar = ''; + + handleBar(event: string) { this.bar = event; } + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent, SuperSuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + + expect(app.foo).toBe('test1'); + expect(app.bar).toBe('test2'); + }); + }); + + describe('host bindings (style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings for styles', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperSuperComponent { + @HostBinding('style.color') + color = 'red'; + } + + class SuperComponent extends SuperSuperComponent { + @HostBinding('style.backgroundColor') + bg = 'black'; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + } + + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent, SuperSuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.style.color).toBe('red'); + expect(queryResult.nativeElement.style.backgroundColor).toBe('black'); + }); + }); + + describe('host bindings (non-style related)', () => { + // TODO: sub and super HostBinding same property but different bindings + // TODO: sub and super HostBinding same binding on two different properties + it('should compose host bindings (non-style related)', () => { + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperSuperComponent { + @HostBinding('title') + get boundTitle() { return this.superTitle + '!!!'; } + + @Input() + superTitle = ''; + } + + class SuperComponent extends SuperSuperComponent { + @HostBinding('accessKey') + get boundAccessKey() { return this.superAccessKey + '???'; } + + @Input() + superAccessKey = ''; + } + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent extends SuperComponent { + } + @Component({ + template: ` + test + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent, SuperSuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const queryResult = fixture.debugElement.query(By.directive(MyComponent)); + + expect(queryResult.nativeElement.tagName).toBe('MY-COMP'); + expect(queryResult.nativeElement.title).toBe('test1!!!'); + expect(queryResult.nativeElement.accessKey).toBe('test2???'); + }); + }); + + it('should inherit ContentChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @ContentChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({ + selector: 'my-comp', + template: `
    `, + }) + class MyComponent extends SuperComponent { + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + +
  • one
  • +
  • two
  • +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, SuperComponent, ChildDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(2); + }); + + it('should inherit ViewChildren queries', () => { + let foundQueryList: QueryList; + + @Directive({selector: '[child-dir]'}) + class ChildDir { + } + + @Component({ + selector: 'super-comp', + template: `

    super

    `, + }) + class SuperComponent { + @ViewChildren(ChildDir) + customDirs !: QueryList; + } + + @Component({ + selector: 'my-comp', + template: ` +
      +
    • {{item}}
    • +
    + `, + }) + class MyComponent extends SuperComponent { + items = [1, 2, 3, 4, 5]; + ngAfterViewInit() { foundQueryList = this.customDirs; } + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent, ChildDir, SuperComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(foundQueryList !.length).toBe(5); + }); + + xdescribe( + 'what happens when...', + () => { + // TODO: sub has Input and super has Output on same property + // TODO: sub has Input and super has HostBinding on same property + // TODO: sub has Input and super has ViewChild on same property + // TODO: sub has Input and super has ViewChildren on same property + // TODO: sub has Input and super has ContentChild on same property + // TODO: sub has Input and super has ContentChildren on same property + // TODO: sub has Output and super has HostBinding on same property + // TODO: sub has Output and super has ViewChild on same property + // TODO: sub has Output and super has ViewChildren on same property + // TODO: sub has Output and super has ContentChild on same property + // TODO: sub has Output and super has ContentChildren on same property + // TODO: sub has HostBinding and super has ViewChild on same property + // TODO: sub has HostBinding and super has ViewChildren on same property + // TODO: sub has HostBinding and super has ContentChild on same property + // TODO: sub has HostBinding and super has ContentChildren on same property + // TODO: sub has ViewChild and super has ViewChildren on same property + // TODO: sub has ViewChild and super has ContentChild on same property + // TODO: sub has ViewChild and super has ContentChildren on same property + // TODO: sub has ViewChildren and super has ContentChild on same property + // TODO: sub has ViewChildren and super has ContentChildren on same property + // TODO: sub has ContentChild and super has ContentChildren on same property + }); }); }); - -@Component({selector: 'app-comp', template: ``}) -class AppComp { -} diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 900a5a2bb2..2a59030167 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -5,12 +5,1402 @@ * 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 {Component, ContentChild, Directive, EventEmitter, HostBinding, HostListener, Input, Output, QueryList, TemplateRef, ViewChildren} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; describe('acceptance integration tests', () => { + function stripHtmlComments(str: string) { return str.replace(//g, ''); } + + describe('render', () => { + + it('should render basic template', () => { + @Component({template: 'Greetings'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + expect(fixture.nativeElement.innerHTML).toEqual('Greetings'); + }); + + it('should render and update basic "Hello, World" template', () => { + ngDevModeResetPerfCounters(); + @Component({template: '

    Hello, {{name}}!

    '}) + class App { + name = ''; + } + + onlyInIvy('perf counters').expectPerfCounters({ + tView: 0, + tNode: 0, + }); + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'World'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    Hello, World!

    '); + onlyInIvy('perf counters').expectPerfCounters({ + tView: 2, // Host view + App + tNode: 4, // Host Node + App Node + + #text + }); + + fixture.componentInstance.name = 'New World'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    Hello, New World!

    '); + // Assert that the tView/tNode count does not increase (they are correctly cached) + onlyInIvy('perf counters').expectPerfCounters({ + tView: 2, + tNode: 4, + }); + + }); + }); + + describe('ng-container', () => { + + it('should insert as a child of a regular element', () => { + @Component( + {template: '
    before|Greetings|after
    '}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + // Strip comments since VE and Ivy put them in different places. + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('
    before|Greetings|after
    '); + }); + + it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { + @Component({ + template: + '
    content
    ' + }) + class App { + render = false; + } + + TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.render = true; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
    content
    '); + + fixture.componentInstance.render = false; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should add and remove DOM nodes when ng-container is a child of an embedded view', () => { + + @Component({template: 'content'}) + class App { + render = false; + } + + TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.render = true; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('content'); + + fixture.componentInstance.render = false; + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + // https://stackblitz.com/edit/angular-tfhcz1?file=src%2Fapp%2Fapp.component.ts + it('should add and remove DOM nodes when ng-container is a child of a delayed embedded view', + () => { + + @Directive({selector: '[testDirective]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} + + createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: 'content' + }) + class App { + @ViewChild(TestDirective, {static: true}) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + }); + + it('should render at the component view root', () => { + @Component( + {selector: 'test-cmpt', template: 'component template'}) + class TestCmpt { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TestCmpt]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('component template'); + }); + + it('should render inside another ng-container', () => { + @Component({ + selector: 'test-cmpt', + template: + 'content' + }) + class TestCmpt { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TestCmpt]}); + const fixture = TestBed.createComponent(App); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)) + .toBe('content'); + }); + + it('should render inside another ng-container at the root of a delayed view', () => { + @Directive({selector: '[testDirective]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} + + createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: + 'content' + }) + class App { + @ViewChild(TestDirective, {static: true}) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); + + fixture.componentInstance.testDirective.createAndInsert(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('contentcontent'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); + }); + + it('should support directives and inject ElementRef', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + constructor(public elRef: ElementRef) {} + } + + @Component({template: '
    '}) + class App { + @ViewChild(TestDirective, {static: false}) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
    '); + expect(fixture.componentInstance.testDirective.elRef.nativeElement.nodeType) + .toBe(Node.COMMENT_NODE); + }); + + it('should support ViewContainerRef when ng-container is at the root of a view', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + @Input() + contentTpl: TemplateRef<{}>|null = null; + + constructor(private _vcRef: ViewContainerRef) {} + + insertView() { this._vcRef.createEmbeddedView(this.contentTpl as TemplateRef<{}>); } + + clear() { this._vcRef.clear(); } + } + + @Component({ + template: + 'Content' + }) + class App { + @ViewChild(TestDirective, {static: false}) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.testDirective.insertView(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should support ViewContainerRef on inside ', () => { + @Directive({selector: '[dir]'}) + class TestDirective { + constructor(private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef) {} + + insertView() { this._vcRef.createEmbeddedView(this._tplRef); } + + clear() { this._vcRef.clear(); } + } + + @Component({template: 'Content'}) + class App { + @ViewChild(TestDirective, {static: false}) testDirective !: TestDirective; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + + fixture.componentInstance.testDirective.insertView(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); + + fixture.componentInstance.testDirective.clear(); + fixture.detectChanges(); + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); + }); + + it('should not set any attributes', () => { + @Component({template: '
    '}) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
    '); + }); + + }); + + describe('text bindings', () => { + it('should render "undefined" as ""', () => { + @Component({template: '{{name}}'}) + class App { + name: string|undefined = 'benoit'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('benoit'); + + fixture.componentInstance.name = undefined; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should render "null" as ""', () => { + @Component({template: '{{name}}'}) + class App { + name: string|null = 'benoit'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('benoit'); + + fixture.componentInstance.name = null; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + }); + + describe('ngNonBindable handling', () => { + function stripNgNonBindable(str: string) { return str.replace(/ ngnonbindable=""/i, ''); } + + it('should keep local ref for host element', () => { + @Component({ + template: ` + + Hello {{ name }}! + + {{ myRef.id }} + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}! my-id '); + }); + + it('should invoke directives for host element', () => { + let directiveInvoked: boolean = false; + + @Directive({selector: '[directive]'}) + class TestDirective implements OnInit { + ngOnInit() { directiveInvoked = true; } + } + + @Component({ + template: ` + + Hello {{ name }}! + + ` + }) + class App { + name = 'World'; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(true); + }); + + it('should not invoke directives for nested elements', () => { + let directiveInvoked: boolean = false; + + @Directive({selector: '[directive]'}) + class TestDirective implements OnInit { + ngOnInit() { directiveInvoked = true; } + } + + @Component({ + template: ` + + Hello {{ name }}! + + ` + }) + class App { + name = 'World'; + } + + TestBed.configureTestingModule({declarations: [App, TestDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) + .toEqual('Hello {{ name }}!'); + expect(directiveInvoked).toEqual(false); + }); + }); + + describe('Siblings update', () => { + it('should handle a flat list of static/bound text nodes', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'monde'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello monde!'); + }); + + it('should handle a list of static/bound text nodes as element children', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); + }); + + it('should render/update text node as a child of a deep list of elements', () => { + @Component({template: 'Hello {{name}}!'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); + }); + + it('should update 2 sibling elements', () => { + @Component({template: ''}) + class App { + id = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.id = 'foo'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + + fixture.componentInstance.id = 'bar'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + }); + + it('should handle sibling text node after element with child text node', () => { + @Component({template: '

    hello

    {{name}}'}) + class App { + name = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + + fixture.componentInstance.name = 'world'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    hello

    world'); + + fixture.componentInstance.name = 'mundo'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    hello

    mundo'); + }); + }); + + describe('basic components', () => { + @Component({selector: 'todo', template: '

    Todo{{value}}

    '}) + class TodoComponent { + value = ' one'; + } + + it('should support a basic component template', () => { + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    Todo one

    '); + }); + + it('should support a component template with sibling', () => { + @Component({template: 'two'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    Todo one

    two'); + }); + + it('should support a component template with component sibling', () => { + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, TodoComponent]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML) + .toEqual('

    Todo one

    Todo one

    '); + }); + + it('should support a component with binding on host element', () => { + @Component({selector: 'todo', template: '{{title}}'}) + class TodoComponentHostBinding { + @HostBinding() + title = 'one'; + } + + @Component({template: ''}) + class App { + @ViewChild(TodoComponentHostBinding, {static: false}) + todoComponentHostBinding !: TodoComponentHostBinding; + } + + TestBed.configureTestingModule({declarations: [App, TodoComponentHostBinding]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('one'); + + fixture.componentInstance.todoComponentHostBinding.title = 'two'; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('two'); + }); + + it('should support root component with host attribute', () => { + @Component({selector: 'host-attr-comp', template: '', host: {'role': 'button'}}) + class HostAttributeComp { + } + + TestBed.configureTestingModule({declarations: [HostAttributeComp]}); + const fixture = TestBed.createComponent(HostAttributeComp); + fixture.detectChanges(); + + expect(fixture.nativeElement.getAttribute('role')).toEqual('button'); + }); + + it('should support component with bindings in template', () => { + @Component({selector: 'comp', template: '

    {{ name }}

    '}) + class MyComp { + name = 'Bess'; + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML).toEqual('

    Bess

    '); + }); + + it('should support a component with sub-views', () => { + @Component({selector: 'comp', template: '
    text
    '}) + class MyComp { + @Input() + condition !: boolean; + } + + @Component({template: ''}) + class App { + condition = false; + } + + TestBed.configureTestingModule({declarations: [App, MyComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const compElement = fixture.nativeElement.querySelector('comp'); + + fixture.componentInstance.condition = true; + fixture.detectChanges(); + expect(stripHtmlComments(compElement.innerHTML)).toEqual('
    text
    '); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + expect(stripHtmlComments(compElement.innerHTML)).toEqual(''); + }); + + }); + + describe('element bindings', () => { + describe('elementAttribute', () => { + it('should support attribute bindings', () => { + @Component({template: ''}) + class App { + title: string|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.title = 'Hello'; + fixture.detectChanges(); + // initial binding + expect(fixture.nativeElement.innerHTML).toEqual(''); + + // update binding + fixture.componentInstance.title = 'Hi!'; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + // remove attribute + fixture.componentInstance.title = null; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should stringify values used attribute bindings', () => { + @Component({template: ''}) + class App { + title: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.title = NaN; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + fixture.componentInstance.title = {toString: () => 'Custom toString'}; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML) + .toEqual(''); + }); + + it('should update bindings', () => { + @Component({ + template: [ + 'a:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', + 'a0:{{c[1]}}', + 'a1:{{c[0]}}{{c[1]}}{{c[16]}}', + 'a2:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[16]}}', + 'a3:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[16]}}', + 'a4:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[16]}}', + 'a5:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[16]}}', + 'a6:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[16]}}', + 'a7:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[16]}}', + 'a8:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', + ].join('\n') + }) + class App { + c = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:(0a1b2c3d4e5f6g7)', + 'a0:0', + 'a1:(0)', + 'a2:(0a1)', + 'a3:(0a1b2)', + 'a4:(0a1b2c3)', + 'a5:(0a1b2c3d4)', + 'a6:(0a1b2c3d4e5)', + 'a7:(0a1b2c3d4e5f6)', + 'a8:(0a1b2c3d4e5f6g7)', + ].join('\n')); + + fixture.componentInstance.c.reverse(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:)7g6f5e4d3c2b1a0(', + 'a0:7', + 'a1:)7(', + 'a2:)7g6(', + 'a3:)7g6f5(', + 'a4:)7g6f5e4(', + 'a5:)7g6f5e4d3(', + 'a6:)7g6f5e4d3c2(', + 'a7:)7g6f5e4d3c2b1(', + 'a8:)7g6f5e4d3c2b1a0(', + ].join('\n')); + + fixture.componentInstance.c.reverse(); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent.trim()).toEqual([ + 'a:(0a1b2c3d4e5f6g7)', + 'a0:0', + 'a1:(0)', + 'a2:(0a1)', + 'a3:(0a1b2)', + 'a4:(0a1b2c3)', + 'a5:(0a1b2c3d4)', + 'a6:(0a1b2c3d4e5)', + 'a7:(0a1b2c3d4e5f6)', + 'a8:(0a1b2c3d4e5f6g7)', + ].join('\n')); + }); + + it('should not update DOM if context has not changed', () => { + @Component({ + template: ` + + + + ` + }) + class App { + title: string|null = ''; + shouldRender = true; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); + const bold: HTMLElement = span.querySelector('b') !; + + fixture.componentInstance.title = 'Hello'; + fixture.detectChanges(); + + // initial binding + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Hello'); + + // update DOM manually + bold.setAttribute('title', 'Goodbye'); + + // refresh with same binding + fixture.detectChanges(); + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Goodbye'); + + // refresh again with same binding + fixture.detectChanges(); + expect(span.getAttribute('title')).toBe('Hello'); + expect(bold.getAttribute('title')).toBe('Goodbye'); + }); + + it('should support host attribute bindings', () => { + @Directive({selector: '[hostBindingDir]'}) + class HostBindingDir { + @HostBinding('attr.aria-label') + label = 'some label'; + } + + @Component({template: '
    '}) + class App { + @ViewChild(HostBindingDir, {static: false}) hostBindingDir !: HostBindingDir; + } + + TestBed.configureTestingModule({declarations: [App, HostBindingDir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const hostBindingEl = fixture.nativeElement.querySelector('div'); + + // Needs `toLowerCase`, because different browsers produce + // attributes either in camel case or lower case. + expect(hostBindingEl.getAttribute('aria-label')).toBe('some label'); + + fixture.componentInstance.hostBindingDir.label = 'other label'; + fixture.detectChanges(); + + expect(hostBindingEl.getAttribute('aria-label')).toBe('other label'); + }); + }); + + describe('elementStyle', () => { + it('should support binding to styles', () => { + @Component({template: ''}) + class App { + size: string|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.size = '10px'; + fixture.detectChanges(); + const span: HTMLElement = fixture.nativeElement.querySelector('span'); + + expect(span.style.fontSize).toBe('10px'); + + fixture.componentInstance.size = '16px'; + fixture.detectChanges(); + expect(span.style.fontSize).toBe('16px'); + + fixture.componentInstance.size = null; + fixture.detectChanges(); + expect(span.style.fontSize).toBeFalsy(); + }); + + it('should support binding to styles with suffix', () => { + @Component({template: ''}) + class App { + size: string|number|null = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.size = '100'; + fixture.detectChanges(); + const span: HTMLElement = fixture.nativeElement.querySelector('span'); + + expect(span.style.fontSize).toEqual('100px'); + + fixture.componentInstance.size = 200; + fixture.detectChanges(); + expect(span.style.fontSize).toEqual('200px'); + + fixture.componentInstance.size = 0; + fixture.detectChanges(); + expect(span.style.fontSize).toEqual('0px'); + + fixture.componentInstance.size = null; + fixture.detectChanges(); + expect(span.style.fontSize).toBeFalsy(); + }); + }); + + describe('class-based styling', () => { + it('should support CSS class toggle', () => { + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const span = fixture.nativeElement.querySelector('span'); + + expect(span.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + + // truthy values + fixture.componentInstance.value = 'a_string'; + fixture.detectChanges(); + expect(span.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = 10; + fixture.detectChanges(); + expect(span.getAttribute('class')).toEqual('active'); + + // falsy values + fixture.componentInstance.value = ''; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + + fixture.componentInstance.value = 0; + fixture.detectChanges(); + expect(span.getAttribute('class')).toBeFalsy(); + }); + + it('should work correctly with existing static classes', () => { + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual(''); + }); + + it('should apply classes properly when nodes are components', () => { + @Component({selector: 'my-comp', template: 'Comp Content'}) + class MyComp { + } + + @Component({template: ''}) + class App { + value: any; + } + + TestBed.configureTestingModule({declarations: [App, MyComp]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const compElement = fixture.nativeElement.querySelector('my-comp'); + + expect(fixture.nativeElement.textContent).toContain('Comp Content'); + expect(compElement.getAttribute('class')).toBe('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(compElement.getAttribute('class')).toBeFalsy(); + }); + + it('should apply classes properly when nodes have containers', () => { + @Component({selector: 'structural-comp', template: 'Comp Content'}) + class StructuralComp { + @Input() + tmp !: TemplateRef; + + constructor(public vcr: ViewContainerRef) {} + + create() { this.vcr.createEmbeddedView(this.tmp); } + } + + @Component({ + template: ` + Temp Content + + ` + }) + class App { + @ViewChild(StructuralComp, {static: false}) structuralComp !: StructuralComp; + value: any; + } + + TestBed.configureTestingModule({declarations: [App, StructuralComp]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = true; + fixture.detectChanges(); + const structuralCompEl = fixture.nativeElement.querySelector('structural-comp'); + + expect(structuralCompEl.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.structuralComp.create(); + fixture.detectChanges(); + expect(structuralCompEl.getAttribute('class')).toEqual('active'); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(structuralCompEl.getAttribute('class')).toEqual(''); + }); + + @Directive({selector: '[DirWithClass]'}) + class DirWithClassDirective { + public classesVal: string = ''; + + @Input('class') + set klass(value: string) { this.classesVal = value; } + } + + @Directive({selector: '[DirWithStyle]'}) + class DirWithStyleDirective { + public stylesVal: string = ''; + + @Input() + set style(value: string) { this.stylesVal = value; } + } + + it('should delegate initial classes to a [class] input binding if present on a directive on the same element', + () => { + @Component({template: '
    '}) + class App { + @ViewChild(DirWithClassDirective, {static: false}) + mockClassDirective !: DirWithClassDirective; + } + + TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.componentInstance.mockClassDirective.classesVal) + .toEqual('apple orange banana'); + }); + + it('should delegate initial styles to a [style] input binding if present on a directive on the same element', + () => { + @Component({template: '
    '}) + class App { + @ViewChild(DirWithStyleDirective, {static: false}) + mockStyleDirective !: DirWithStyleDirective; + } + + TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const styles = fixture.componentInstance.mockStyleDirective.stylesVal; + + // Use `toContain` since Ivy and ViewEngine have some slight differences in formatting. + expect(styles).toContain('width:100px'); + expect(styles).toContain('height:200px'); + }); + + it('should update `[class]` and bindings in the provided directive if the input is matched', + () => { + @Component({template: '
    '}) + class App { + @ViewChild(DirWithClassDirective, {static: false}) + mockClassDirective !: DirWithClassDirective; + value = ''; + } + + TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = 'cucumber grape'; + fixture.detectChanges(); + + expect(fixture.componentInstance.mockClassDirective.classesVal) + .toEqual('cucumber grape'); + }); + + onlyInIvy('Passing an object into [style] works differently') + .it('should update `[style]` and bindings in the provided directive if the input is matched', + () => { + @Component({template: '
    '}) + class App { + @ViewChild(DirWithStyleDirective, {static: false}) + mockStyleDirective !: DirWithStyleDirective; + value !: {[key: string]: string}; + } + + TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); + const fixture = TestBed.createComponent(App); + fixture.componentInstance.value = {width: '200px', height: '500px'}; + fixture.detectChanges(); + + expect(fixture.componentInstance.mockStyleDirective.stylesVal) + .toEqual('width:200px;height:500px'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should apply initial styling to the element that contains the directive with host styling', + () => { + @Directive({ + selector: '[DirWithInitialStyling]', + host: { + 'title': 'foo', + 'class': 'heavy golden', + 'style': 'color: purple', + '[style.font-weight]': '"bold"' + } + }) + class DirWithInitialStyling { + } + + @Component({ + template: ` +
    + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, DirWithInitialStyling]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + const classes = target.getAttribute('class') !.split(/\s+/).sort(); + expect(classes).toEqual(['big', 'golden', 'heavy']); + + expect(target.getAttribute('title')).toEqual('foo'); + expect(target.style.getPropertyValue('color')).toEqual('black'); + expect(target.style.getPropertyValue('font-size')).toEqual('200px'); + expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', + () => { + @Directive({ + selector: '[DirWithSingleStylingBindings]', + host: { + 'class': 'def', + '[class.xyz]': 'activateXYZClass', + '[style.width]': 'width', + '[style.height]': 'height' + } + }) + class DirWithSingleStylingBindings { + width: null|string = null; + height: null|string = null; + activateXYZClass: boolean = false; + } + + @Component({ + template: ` +
    + ` + }) + class App { + @ViewChild(DirWithSingleStylingBindings, {static: false}) + dirInstance !: DirWithSingleStylingBindings; + } + + TestBed.configureTestingModule({declarations: [App, DirWithSingleStylingBindings]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const dirInstance = fixture.componentInstance.dirInstance; + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeFalsy(); + + dirInstance.width = '444px'; + dirInstance.height = '999px'; + dirInstance.activateXYZClass = true; + fixture.detectChanges(); + + expect(target.style.getPropertyValue('width')).toEqual('444px'); + expect(target.style.getPropertyValue('height')).toEqual('999px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + + dirInstance.width = null; + dirInstance.height = null; + fixture.detectChanges(); + + expect(target.style.getPropertyValue('width')).toEqual('100px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('abc')).toBeTruthy(); + expect(target.classList.contains('def')).toBeTruthy(); + expect(target.classList.contains('xyz')).toBeTruthy(); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should properly prioritize single style binding collisions when they exist on multiple directives', + () => { + @Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}}) + class Dir1WithStyle { + width: null|string = null; + } + + @Directive({ + selector: '[Dir2WithStyle]', + host: {'style': 'width: 111px', '[style.width]': 'width'} + }) + class Dir2WithStyle { + width: null|string = null; + } + + @Component( + {template: '
    '}) + class App { + @ViewChild(Dir1WithStyle, {static: false}) dir1Instance !: Dir1WithStyle; + @ViewChild(Dir2WithStyle, {static: false}) dir2Instance !: Dir2WithStyle; + width: string|null = null; + } + + TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {dir1Instance, dir2Instance} = fixture.componentInstance; + + const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + fixture.componentInstance.width = '999px'; + dir1Instance.width = '222px'; + dir2Instance.width = '333px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + + fixture.componentInstance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + + dir1Instance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + + dir2Instance.width = null; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + dir1Instance.width = '666px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + + fixture.componentInstance.width = '777px'; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + }); + + onlyInIvy('Style binding merging works differently in Ivy') + .it('should properly prioritize multi style binding collisions when they exist on multiple directives', + () => { + @Directive({ + selector: '[Dir1WithStyling]', + host: {'[style]': 'stylesExp', '[class]': 'classesExp'} + }) + class Dir1WithStyling { + classesExp: any = {}; + stylesExp: any = {}; + } + + @Directive({ + selector: '[Dir2WithStyling]', + host: {'style': 'width: 111px', '[style]': 'stylesExp'} + }) + class Dir2WithStyling { + stylesExp: any = {}; + } + + @Component({ + template: + '
    ' + }) + class App { + @ViewChild(Dir1WithStyling, {static: false}) dir1Instance !: Dir1WithStyling; + @ViewChild(Dir2WithStyling, {static: false}) dir2Instance !: Dir2WithStyling; + stylesExp: any = {}; + classesExp: any = {}; + } + + TestBed.configureTestingModule( + {declarations: [App, Dir1WithStyling, Dir2WithStyling]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const {dir1Instance, dir2Instance} = fixture.componentInstance; + + const target = fixture.nativeElement.querySelector('div') !; + expect(target.style.getPropertyValue('width')).toEqual('111px'); + + const compInstance = fixture.componentInstance; + compInstance.stylesExp = {width: '999px', height: null}; + compInstance.classesExp = {one: true, two: false}; + dir1Instance.stylesExp = {width: '222px'}; + dir1Instance.classesExp = {two: true, three: false}; + dir2Instance.stylesExp = {width: '333px', height: '100px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('999px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + compInstance.stylesExp = {}; + compInstance.classesExp = {}; + dir1Instance.stylesExp = {width: '222px', height: '200px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('222px'); + expect(target.style.getPropertyValue('height')).toEqual('200px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeTruthy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir1Instance.stylesExp = {}; + dir1Instance.classesExp = {}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('333px'); + expect(target.style.getPropertyValue('height')).toEqual('100px'); + expect(target.classList.contains('one')).toBeFalsy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + + dir2Instance.stylesExp = {}; + compInstance.stylesExp = {height: '900px'}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('111px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + + dir1Instance.stylesExp = {width: '666px', height: '600px'}; + dir1Instance.classesExp = {four: true, one: true}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('666px'); + expect(target.style.getPropertyValue('height')).toEqual('900px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeTruthy(); + + compInstance.stylesExp = {width: '777px'}; + compInstance.classesExp = {four: false}; + fixture.detectChanges(); + expect(target.style.getPropertyValue('width')).toEqual('777px'); + expect(target.style.getPropertyValue('height')).toEqual('600px'); + expect(target.classList.contains('one')).toBeTruthy(); + expect(target.classList.contains('two')).toBeFalsy(); + expect(target.classList.contains('three')).toBeFalsy(); + expect(target.classList.contains('four')).toBeFalsy(); + }); + }); + + it('should properly handle and render interpolation for class attribute bindings', () => { + @Component({template: '
    '}) + class App { + name = ''; + age = ''; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + const target = fixture.nativeElement.querySelector('div') !; + + expect(target.classList.contains('-fred-36-')).toBeFalsy(); + + fixture.componentInstance.name = 'fred'; + fixture.componentInstance.age = '36'; + fixture.detectChanges(); + + expect(target.classList.contains('-fred-36-')).toBeTruthy(); + }); + }); + it('should only call inherited host listeners once', () => { let clicks = 0; @@ -106,7 +1496,7 @@ describe('acceptance integration tests', () => { @Component( {selector: 'test-component', template: `foo`, host: {'[attr.aria-disabled]': 'true'}}) class TestComponent { - @ContentChild(TemplateRef) tpl !: TemplateRef; + @ContentChild(TemplateRef, {static: true}) tpl !: TemplateRef; } TestBed.configureTestingModule({declarations: [TestComponent]}); diff --git a/packages/core/test/acceptance/lifecycle_spec.ts b/packages/core/test/acceptance/lifecycle_spec.ts index f5ce6fd7d8..77d6d37161 100644 --- a/packages/core/test/acceptance/lifecycle_spec.ts +++ b/packages/core/test/acceptance/lifecycle_spec.ts @@ -6,10 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, ComponentFactoryResolver, Directive, Input, NgModule, OnChanges, SimpleChanges, ViewChild, ViewContainerRef} from '@angular/core'; +import {SimpleChange} from '@angular/core/src/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {onlyInIvy} from '@angular/private/testing'; -describe('ngOnChanges', () => { +describe('onChanges', () => { it('should correctly support updating one Input among many', () => { let log: string[] = []; @@ -56,6 +60,930 @@ describe('ngOnChanges', () => { fixture.detectChanges(); expect(log).toEqual(['c: 0 -> 3']); }); + + it('should call onChanges method after inputs are set in creation and update mode', () => { + const events: any[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + val1 = 'a'; + + @Input('publicVal2') + val2 = 'b'; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp', changes}); } + } + + @Component({template: ``}) + class App { + val1 = 'a2'; + + val2 = 'b2'; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([{ + name: 'comp', + changes: { + val1: new SimpleChange(undefined, 'a2', true), + val2: new SimpleChange(undefined, 'b2', true), + } + }]); + + events.length = 0; + fixture.componentInstance.val1 = 'a3'; + fixture.componentInstance.val2 = 'b3'; + fixture.detectChanges(); + + + expect(events).toEqual([{ + name: 'comp', + changes: { + val1: new SimpleChange('a2', 'a3', false), + val2: new SimpleChange('b2', 'b3', false), + } + }]); + }); + + it('should call parent onChanges before child onChanges', () => { + const events: any[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'parent', changes}); } + } + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'child', changes}); } + } + + @Component({template: ``}) + class App { + val = 'foo'; + } + + TestBed.configureTestingModule({ + declarations: [App, Child, Parent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent', + changes: { + val: new SimpleChange(undefined, 'foo', true), + } + }, + { + name: 'child', + changes: { + val: new SimpleChange(undefined, 'foo', true), + } + } + ]); + + events.length = 0; + fixture.componentInstance.val = 'bar'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + }, + { + name: 'child', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + } + ]); + }); + + it('should call all parent onChanges across view before calling children onChanges', () => { + const events: any[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'parent ' + this.name, changes}); } + } + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'child ' + this.name, changes}); } + } + + @Component({ + template: ` + + + ` + }) + class App { + val = 'foo'; + } + + TestBed.configureTestingModule({ + declarations: [App, Child, Parent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'foo', true), + } + }, + { + name: 'parent 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'foo', true), + } + }, + { + name: 'child 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'foo', true), + } + }, + { + name: 'child 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'foo', true), + } + }, + ]); + + events.length = 0; + fixture.componentInstance.val = 'bar'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent 1', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + }, + { + name: 'parent 2', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + }, + { + name: 'child 1', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + }, + { + name: 'child 2', + changes: { + val: new SimpleChange('foo', 'bar', false), + } + }, + ]); + }); + + it('should call onChanges every time a new view is created with ngIf', () => { + const events: any[] = []; + + @Component({ + selector: 'comp', + template: `

    {{val}}

    `, + }) + class Comp { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp', changes}); } + } + + @Component({template: ``}) + class App { + show = true; + + val = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([{ + name: 'comp', + changes: { + val: new SimpleChange(undefined, 'a', true), + } + }]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.val = 'b'; + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(events).toEqual([{ + name: 'comp', + changes: { + val: new SimpleChange(undefined, 'b', true), + } + }]); + }); + + it('should call onChanges in hosts before their content children', () => { + const events: any[] = []; + @Component({ + selector: 'projected', + template: `

    {{val}}

    `, + }) + class Projected { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'projected', changes}); } + } + + @Component({ + selector: 'comp', + template: `
    `, + }) + class Comp { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp', changes}); } + } + + @Component({ + template: ``, + }) + class App { + val = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp', + changes: { + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'projected', + changes: { + val: new SimpleChange(undefined, 'a', true), + } + }, + ]); + + events.length = 0; + fixture.componentInstance.val = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'projected', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + ]); + }); + + it('should call onChanges in host and its content children before next host', () => { + const events: any[] = []; + @Component({ + selector: 'projected', + template: `

    {{val}}

    `, + }) + class Projected { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { + events.push({name: 'projected ' + this.name, changes}); + } + } + + @Component({ + selector: 'comp', + template: `
    `, + }) + class Comp { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp ' + this.name, changes}); } + } + + @Component({ + template: ` + + + + + + + `, + }) + class App { + val = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'projected 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'comp 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'projected 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + ]); + + events.length = 0; + fixture.componentInstance.val = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp 1', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'projected 1', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'comp 2', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'projected 2', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + ]); + }); + + it('should be called on directives after component', () => { + const events: any[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input() + dir = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'dir', changes}); } + } + + @Component({ + selector: 'comp', + template: `

    {{val}}

    `, + }) + class Comp { + @Input() + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp', changes}); } + } + + @Component({ + template: ``, + }) + class App { + val = 'a'; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp', + changes: { + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'dir', + changes: { + dir: new SimpleChange(undefined, 'a', true), + } + } + ]); + + events.length = 0; + fixture.componentInstance.val = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'dir', + changes: { + dir: new SimpleChange('a', 'b', false), + } + } + ]); + }); + + it('should be called on directives on an element', () => { + const events: any[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input() + dir = ''; + + @Input('dir-val') + val = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'dir', changes}); } + } + + @Component({template: `
    `}) + class App { + val1 = 'a'; + val2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([{ + name: 'dir', + changes: { + dir: new SimpleChange(undefined, 'a', true), + val: new SimpleChange(undefined, 'b', true), + } + }]); + + events.length = 0; + fixture.componentInstance.val1 = 'a1'; + fixture.componentInstance.val2 = 'b1'; + fixture.detectChanges(); + expect(events).toEqual([{ + name: 'dir', + changes: { + dir: new SimpleChange('a', 'a1', false), + val: new SimpleChange('b', 'b1', false), + } + }]); + }); + + it('should call onChanges properly in for loop', () => { + const events: any[] = []; + + @Component({ + selector: 'comp', + template: `

    {{val}}

    `, + }) + class Comp { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'comp ' + this.name, changes}); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + val = 'a'; + + numbers = ['2', '3', '4']; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp 0', + changes: { + name: new SimpleChange(undefined, '0', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'comp 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'comp 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'comp 3', + changes: { + name: new SimpleChange(undefined, '3', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'comp 4', + changes: { + name: new SimpleChange(undefined, '4', true), + val: new SimpleChange(undefined, 'a', true), + } + } + ]); + + events.length = 0; + fixture.componentInstance.val = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'comp 0', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'comp 1', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'comp 2', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'comp 3', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'comp 4', + changes: { + val: new SimpleChange('a', 'b', false), + } + } + ]); + }); + + it('should call onChanges properly in for loop with children', () => { + const events: any[] = []; + + @Component({ + selector: 'child', + template: `

    {{val}}

    `, + }) + class Child { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { + events.push({name: 'child of parent ' + this.name, changes}); + } + } + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + val = ''; + + @Input() + name = ''; + + ngOnChanges(changes: SimpleChanges) { events.push({name: 'parent ' + this.name, changes}); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + val = 'a'; + numbers = ['2', '3', '4']; + } + + TestBed.configureTestingModule({ + declarations: [App, Child, Parent], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent 0', + changes: { + name: new SimpleChange(undefined, '0', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'parent 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'parent 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'child of parent 2', + changes: { + name: new SimpleChange(undefined, '2', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'parent 3', + changes: { + name: new SimpleChange(undefined, '3', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'child of parent 3', + changes: { + name: new SimpleChange(undefined, '3', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'parent 4', + changes: { + name: new SimpleChange(undefined, '4', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'child of parent 4', + changes: { + name: new SimpleChange(undefined, '4', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'child of parent 0', + changes: { + name: new SimpleChange(undefined, '0', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + { + name: 'child of parent 1', + changes: { + name: new SimpleChange(undefined, '1', true), + val: new SimpleChange(undefined, 'a', true), + } + }, + ]); + + events.length = 0; + fixture.componentInstance.val = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + { + name: 'parent 0', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'parent 1', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'parent 2', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'child of parent 2', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'parent 3', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'child of parent 3', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'parent 4', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'child of parent 4', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'child of parent 0', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + { + name: 'child of parent 1', + changes: { + val: new SimpleChange('a', 'b', false), + } + }, + ]); + }); + + it('should not call onChanges if props are set directly', () => { + const events: any[] = []; + + @Component({template: `

    {{value}}

    `}) + class App { + value = 'a'; + ngOnChanges(changes: SimpleChanges) { events.push(changes); } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([]); + }); }); it('should call all hooks in correct order when several directives on same node', () => { @@ -169,3 +1097,2614 @@ it('should call hooks after setting directives inputs', () => { fixture.detectChanges(); expect(log).toEqual(['doCheckB1', 'doCheckC1', 'doCheckB1', 'doCheckC1']); }); + +describe('onInit', () => { + it('should call onInit after inputs are the first time', () => { + const input1Values: string[] = []; + const input2Values: string[] = []; + + @Component({ + selector: 'my-comp', + template: `

    test

    `, + }) + class MyComponent { + @Input() + input1 = ''; + + @Input() + input2 = ''; + + ngOnInit() { + input1Values.push(this.input1); + input2Values.push(this.input2); + } + } + + @Component({ + template: ` + + `, + }) + class App { + value1 = 'a'; + value2 = 'b'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComponent], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(input1Values).toEqual(['a']); + expect(input2Values).toEqual(['b']); + + fixture.componentInstance.value1 = 'c'; + fixture.componentInstance.value2 = 'd'; + fixture.detectChanges(); + + // Shouldn't be called again just because change detection ran. + expect(input1Values).toEqual(['a']); + expect(input2Values).toEqual(['b']); + }); + + it('should be called on root component', () => { + let onInitCalled = 0; + + @Component({template: ``}) + class App { + ngOnInit() { onInitCalled++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(onInitCalled).toBe(1); + }); + + it('should call parent onInit before it calls child onInit', () => { + const initCalls: string[] = []; + + @Component({ + selector: `child-comp`, + template: `

    child

    `, + }) + class ChildComp { + ngOnInit() { initCalls.push('child'); } + } + + @Component({ + template: ``, + }) + class ParentComp { + ngOnInit() { initCalls.push('parent'); } + } + + TestBed.configureTestingModule({ + declarations: [ParentComp, ChildComp], + }); + const fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + expect(initCalls).toEqual(['parent', 'child']); + }); + + it('should call all parent onInits across view before calling children onInits', () => { + const initCalls: string[] = []; + + @Component({ + selector: `child-comp`, + template: `

    child

    `, + }) + class ChildComp { + @Input() + name = ''; + + ngOnInit() { initCalls.push(`child of parent ${this.name}`); } + } + + @Component({ + selector: 'parent-comp', + template: ``, + }) + class ParentComp { + @Input() + name = ''; + + ngOnInit() { initCalls.push(`parent ${this.name}`); } + } + + @Component({ + template: ` + + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, ParentComp, ChildComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initCalls).toEqual(['parent 1', 'parent 2', 'child of parent 1', 'child of parent 2']); + }); + + it('should call onInit every time a new view is created (if block)', () => { + let onInitCalls = 0; + + @Component({selector: 'my-comp', template: '

    test

    '}) + class MyComp { + ngOnInit() { onInitCalls++; } + } + + @Component({ + template: ` +
    + ` + }) + class App { + show = true; + } + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(onInitCalls).toBe(1); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(onInitCalls).toBe(1); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(onInitCalls).toBe(2); + }); + + it('should call onInit for children of dynamically created components', () => { + @Component({selector: 'my-comp', template: '

    test

    '}) + class MyComp { + onInitCalled = false; + + ngOnInit() { this.onInitCalled = true; } + } + + @Component({ + selector: 'dynamic-comp', + template: ` + + `, + }) + class DynamicComp { + } + + @Component({ + template: ` +
    + `, + }) + class App { + @ViewChild('container', {read: ViewContainerRef, static: false}) + viewContainerRef !: ViewContainerRef; + + constructor(public compFactoryResolver: ComponentFactoryResolver) {} + + createDynamicView() { + const dynamicCompFactory = this.compFactoryResolver.resolveComponentFactory(DynamicComp); + this.viewContainerRef.createComponent(dynamicCompFactory); + } + } + + // View Engine requires that DynamicComp be in entryComponents. + @NgModule({ + declarations: [App, MyComp, DynamicComp], + entryComponents: [DynamicComp, App], + }) + class AppModule { + } + + TestBed.configureTestingModule({imports: [AppModule]}); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + fixture.componentInstance.createDynamicView(); + fixture.detectChanges(); + + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + expect(myComp.onInitCalled).toBe(true); + }); + + it('should call onInit in hosts before their content children', () => { + const initialized: string[] = []; + + @Component({ + selector: 'projected', + template: '', + }) + class Projected { + ngOnInit() { initialized.push('projected'); } + } + + @Component({ + selector: 'comp', + template: ``, + }) + class Comp { + ngOnInit() { initialized.push('comp'); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp', 'projected']); + }); + + + it('should call onInit in host and its content children before next host', () => { + const initialized: string[] = []; + + @Component({ + selector: 'projected', + template: '', + }) + class Projected { + @Input() + name = ''; + + ngOnInit() { initialized.push('projected ' + this.name); } + } + + @Component({ + selector: 'comp', + template: ``, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp 1', 'projected 1', 'comp 2', 'projected 2']); + }); + + it('should be called on directives after component', () => { + const initialized: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir-name') + name = ''; + + ngOnInit() { initialized.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'comp 1', 'dir 1', 'comp 2', 'dir 2']); + }); + + it('should be called on directives on an element', () => { + const initialized: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir-name') + name = ''; + + ngOnInit() { initialized.push('dir ' + this.name); } + } + + @Component({ + template: ` +

    +

    + ` + }) + class App { + ngOnInit() { initialized.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual(['app', 'dir 1', 'dir 2']); + }); + + + it('should call onInit properly in for loop', () => { + const initialized: string[] = []; + + @Component({ + selector: 'comp', + template: `

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnInit() { initialized.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [2, 3, 4, 5, 6]; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual([ + 'comp 0', 'comp 1', 'comp 2', 'comp 3', 'comp 4', 'comp 5', 'comp 6' + ]); + }); + + it('should call onInit properly in for loop with children', () => { + const initialized: string[] = []; + + @Component({ + selector: 'child', + template: `

    `, + }) + class Child { + @Input() + name = ''; + + ngOnInit() { initialized.push('child of parent ' + this.name); } + } + + @Component({selector: 'parent', template: ''}) + class Parent { + @Input() + name = ''; + + ngOnInit() { initialized.push('parent ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [2, 3, 4, 5, 6]; + } + + TestBed.configureTestingModule({ + declarations: [App, Child, Parent], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(initialized).toEqual([ + // First the two root level components + 'parent 0', + 'parent 1', + + // Then our 5 embedded views + 'parent 2', + 'child of parent 2', + 'parent 3', + 'child of parent 3', + 'parent 4', + 'child of parent 4', + 'parent 5', + 'child of parent 5', + 'parent 6', + 'child of parent 6', + + // Then the children of the root level components + 'child of parent 0', + 'child of parent 1', + ]); + }); +}); + +describe('doCheck', () => { + it('should call doCheck on every refresh', () => { + let doCheckCalled = 0; + + @Component({template: ``}) + class App { + ngDoCheck() { doCheckCalled++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(doCheckCalled).toBe(1); + + fixture.detectChanges(); + + expect(doCheckCalled).toBe(2); + }); + + it('should call parent doCheck before child doCheck', () => { + const doChecks: string[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + ngDoCheck() { doChecks.push('parent'); } + } + + @Component({ + selector: 'child', + template: ``, + }) + class Child { + ngDoCheck() { doChecks.push('child'); } + } + + @Component({template: ``}) + class App { + ngDoCheck() { doChecks.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(doChecks).toEqual(['app', 'parent', 'child']); + }); + + it('should call ngOnInit before ngDoCheck if creation mode', () => { + const events: string[] = []; + @Component({template: ``}) + class App { + ngOnInit() { events.push('onInit'); } + + ngDoCheck() { events.push('doCheck'); } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual(['onInit', 'doCheck']); + }); + + it('should be called on directives after component', () => { + const doChecks: string[] = []; + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngDoCheck() { doChecks.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngDoCheck() { doChecks.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngDoCheck() { doChecks.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(doChecks).toEqual(['app', 'comp 1', 'dir 1', 'comp 2', 'dir 2']); + }); + + it('should be called on directives on an element', () => { + const doChecks: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngDoCheck() { doChecks.push('dir ' + this.name); } + } + + @Component({ + template: ` +

    +

    + ` + }) + class App { + ngDoCheck() { doChecks.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(doChecks).toEqual(['app', 'dir 1', 'dir 2']); + }); +}); + +describe('afterContentinit', () => { + it('should be called only in creation mode', () => { + let afterContentInitCalls = 0; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterContentInit() { afterContentInitCalls++; } + } + @Component({template: ``}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // two updates + fixture.detectChanges(); + fixture.detectChanges(); + + expect(afterContentInitCalls).toBe(1); + }); + + it('should be called on root component in creation mode', () => { + let afterContentInitCalls = 0; + + @Component({template: `

    test

    `}) + class App { + ngAfterContentInit() { afterContentInitCalls++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // two updates + fixture.detectChanges(); + fixture.detectChanges(); + + expect(afterContentInitCalls).toBe(1); + }); + + it('should be called on every create ngIf', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterContentInit() { events.push('comp afterContentInit'); } + } + + @Component({template: ``}) + class App { + show = true; + + ngAfterContentInit() { events.push('app afterContentInit'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual(['app afterContentInit', 'comp afterContentInit']); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual(['app afterContentInit', 'comp afterContentInit']); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + + expect(events).toEqual( + ['app afterContentInit', 'comp afterContentInit', 'comp afterContentInit']); + }); + + it('should be called in parents before children', () => { + const events: string[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + + ngAfterContentInit() { events.push('parent ' + this.name); } + } + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngAfterContentInit() { events.push('child of parent ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterContentInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual( + ['app', 'parent 1', 'parent 2', 'child of parent 1', 'child of parent 2']); + }); + + it('should be called in projected components before their hosts', () => { + const events: string[] = []; + + @Component({ + selector: 'projected-child', + template: `

    test

    `, + }) + class ProjectedChild { + @Input() + name = ''; + + ngAfterContentInit() { events.push('projected child ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `
    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterContentInit() { events.push('comp ' + this.name); } + } + + @Component({ + selector: 'projected', + template: ``, + }) + class Projected { + @Input() + name = ''; + + ngAfterContentInit() { events.push('projected ' + this.name); } + } + + @Component({ + template: ` + + + + + + + + + ` + }) + class App { + ngAfterContentInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected, ProjectedChild], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + // root + 'app', + // projections of comp 1 + 'projected 1', + 'projected 2', + // comp 1 + 'comp 1', + // projections of comp 2 + 'projected 3', + 'projected 4', + // comp 2 + 'comp 2', + // children of projections + 'projected child 1', + 'projected child 2', + 'projected child 3', + 'projected child 4', + ]); + }); + + it('should be called in correct order in a for loop', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterContentInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [0, 1, 2, 3]; + + ngAfterContentInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual(['app', 'comp 0', 'comp 1', 'comp 2', 'comp 3', 'comp 4', 'comp 5']); + }); + + it('should be called in correct order in a for loop with children', () => { + const events: string[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + + ngAfterContentInit() { events.push('parent ' + this.name); } + } + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngAfterContentInit() { events.push('child of parent ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [0, 1, 2, 3]; + ngAfterContentInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + // root + 'app', + // 4 embedded views + 'parent 0', + 'child of parent 0', + 'parent 1', + 'child of parent 1', + 'parent 2', + 'child of parent 2', + 'parent 3', + 'child of parent 3', + // root children + 'parent 4', + 'parent 5', + // children of root children + 'child of parent 4', + 'child of parent 5', + ]); + }); + + it('should be called on directives after component', () => { + const events: string[] = []; + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngAfterContentInit() { events.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterContentInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterContentInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'app', + 'comp 1', + 'dir 1', + 'comp 2', + 'dir 2', + ]); + }); +}); + +describe('afterContentChecked', () => { + it('should be called every change detection run after afterContentInit', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterContentInit() { events.push('comp afterContentInit'); } + + ngAfterContentChecked() { events.push('comp afterContentChecked'); } + } + + @Component({template: ``}) + class App { + ngAfterContentInit() { events.push('app afterContentInit'); } + + ngAfterContentChecked() { events.push('app afterContentChecked'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'app afterContentInit', + 'app afterContentChecked', + 'comp afterContentInit', + 'comp afterContentChecked', + ]); + }); +}); + +describe('afterViewInit', () => { + it('should be called on creation and not in update mode', () => { + let afterViewInitCalls = 0; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterViewInit() { afterViewInitCalls++; } + } + + @Component({template: ``}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // two updates + fixture.detectChanges(); + fixture.detectChanges(); + + expect(afterViewInitCalls).toBe(1); + + }); + + it('should be called on root component in creation mode', () => { + let afterViewInitCalls = 0; + + @Component({template: `

    test

    `}) + class App { + ngAfterViewInit() { afterViewInitCalls++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + // two updates + fixture.detectChanges(); + fixture.detectChanges(); + + expect(afterViewInitCalls).toBe(1); + }); + + it('should be called every time a view is initialized with ngIf', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterViewInit() { events.push('comp'); } + } + + @Component({ + template: ``, + }) + class App { + show = true; + + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual(['comp', 'app']); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual(['comp', 'app']); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(events).toEqual(['comp', 'app', 'comp']); + }); + + it('should be called in children before parents', () => { + const events: string[] = []; + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + + ngAfterViewInit() { events.push('parent ' + this.name); } + } + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngAfterViewInit() { events.push('child of parent ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent 1', + 'child of parent 2', + 'parent 1', + 'parent 2', + 'app', + ]); + }); + + it('should be called in projected components before their hosts', () => { + const events: string[] = []; + + @Component({ + selector: 'projected', + template: `

    test

    `, + }) + class Projected { + @Input() + name = ''; + + ngAfterViewInit() { events.push('projected ' + this.name); } + } + + @Component({ + selector: 'comp', + template: ``, + }) + class Comp { + @Input() + name = ''; + + ngAfterViewInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + + expect(events).toEqual([ + 'projected 1', + 'comp 1', + 'projected 2', + 'comp 2', + 'app', + ]); + }); + + it('should call afterViewInit in content children and host before next host', () => { + const events: string[] = []; + + @Component({ + selector: 'projected-child', + template: `

    test

    `, + }) + class ProjectedChild { + @Input() + name = ''; + + ngAfterViewInit() { events.push('child of projected ' + this.name); } + } + + @Component({ + selector: 'projected', + template: ``, + }) + class Projected { + @Input() + name = ''; + + ngAfterViewInit() { events.push('projected ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `
    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterViewInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected, ProjectedChild], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of projected 1', + 'child of projected 2', + 'projected 1', + 'comp 1', + 'projected 2', + 'comp 2', + 'app', + ]); + }); + + it('should be called in correct order with ngFor', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterViewInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [0, 1, 2, 3]; + + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 0', + 'comp 1', + 'comp 2', + 'comp 3', + 'comp 4', + 'comp 5', + 'app', + ]); + }); + + it('should be called in correct order with for loops with children', () => { + const events: string[] = []; + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngAfterViewInit() { events.push('child of parent ' + this.name); } + } + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + + ngAfterViewInit() { events.push('parent ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [0, 1, 2, 3]; + + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent 0', + 'parent 0', + 'child of parent 1', + 'parent 1', + 'child of parent 2', + 'parent 2', + 'child of parent 3', + 'parent 3', + 'child of parent 4', + 'child of parent 5', + 'parent 4', + 'parent 5', + 'app', + ]); + }); + + it('should be called on directives after component', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngAfterViewInit() { events.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterViewInit() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 1', + 'dir 1', + 'comp 2', + 'dir 2', + 'app', + ]); + }); + + it('should be called on directives on an element', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngAfterViewInit() { events.push('dir ' + this.name); } + } + + @Component({ + template: ` +
    +
    + ` + }) + class App { + ngAfterViewInit() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'dir 1', + 'dir 2', + 'app', + ]); + }); +}); + +describe('afterViewChecked', () => { + it('should call ngAfterViewChecked every update', () => { + let afterViewCheckedCalls = 0; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngAfterViewChecked() { afterViewCheckedCalls++; } + } + + @Component({template: ``}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(1); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(2); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(3); + }); + + it('should be called on root component', () => { + let afterViewCheckedCalls = 0; + + @Component({template: `

    test

    `}) + class App { + ngAfterViewChecked() { afterViewCheckedCalls++; } + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(1); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(2); + + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(3); + }); + + it('should call ngAfterViewChecked with bindings', () => { + let afterViewCheckedCalls = 0; + + @Component({ + selector: 'comp', + template: `

    {{value}}

    `, + }) + class Comp { + @Input() + value = ''; + ngAfterViewChecked() { afterViewCheckedCalls++; } + } + + @Component({template: ``}) + class App { + value = 1; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(1); + + fixture.componentInstance.value = 1337; + fixture.detectChanges(); + expect(afterViewCheckedCalls).toBe(2); + }); + + it('should be called in correct order with for loops with children', () => { + const events: string[] = []; + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngAfterViewChecked() { events.push('child of parent ' + this.name); } + } + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + + ngAfterViewChecked() { events.push('parent ' + this.name); } + } + + @Component({ + template: ` + + + + ` + }) + class App { + numbers = [0, 1, 2, 3]; + + ngAfterViewChecked() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent 0', + 'parent 0', + 'child of parent 1', + 'parent 1', + 'child of parent 2', + 'parent 2', + 'child of parent 3', + 'parent 3', + 'child of parent 4', + 'child of parent 5', + 'parent 4', + 'parent 5', + 'app', + ]); + }); + + it('should be called on directives after component', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngAfterViewChecked() { events.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngAfterViewChecked() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` + + + ` + }) + class App { + ngAfterViewChecked() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 1', + 'dir 1', + 'comp 2', + 'dir 2', + 'app', + ]); + }); + + it('should be called on directives on an element', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngAfterViewChecked() { events.push('dir ' + this.name); } + } + + @Component({ + template: ` +
    +
    + ` + }) + class App { + ngAfterViewChecked() { events.push('app'); } + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'dir 1', + 'dir 2', + 'app', + ]); + }); + +}); + +describe('onDestroy', () => { + + + it('should call destroy when view is removed', () => { + let destroyCalled = 0; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngOnDestroy() { destroyCalled++; } + } + + @Component({ + template: ``, + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(destroyCalled).toBe(0); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(destroyCalled).toBe(1); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(destroyCalled).toBe(1); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(destroyCalled).toBe(2); + }); + + it('should call destroy when multiple views are removed', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnDestroy() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` +
    + + +
    + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual(['comp 1', 'comp 2']); + }); + + it('should be called in child components before parent components', () => { + const events: string[] = []; + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngOnDestroy() { events.push('child of parent ' + this.name); } + } + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + ngOnDestroy() { events.push('parent ' + this.name); } + } + + @Component({ + template: ` +
    + + +
    + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Child], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent 1', + 'child of parent 2', + 'parent 1', + 'parent 2', + ]); + }); + + it('should be called bottom up with children nested 2 levels deep', () => { + const events: string[] = []; + + @Component({ + selector: 'child', + template: `

    test

    `, + }) + class Child { + @Input() + name = ''; + + ngOnDestroy() { events.push('child ' + this.name); } + } + + @Component({ + selector: 'parent', + template: ``, + }) + class Parent { + @Input() + name = ''; + ngOnDestroy() { events.push('parent ' + this.name); } + } + + @Component({ + selector: 'grandparent', + template: ``, + }) + class Grandparent { + @Input() + name = ''; + + ngOnDestroy() { events.push('grandparent ' + this.name); } + } + @Component({ + template: ` +
    + + +
    + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Grandparent, Parent, Child], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child 1', + 'parent 1', + 'child 2', + 'parent 2', + 'grandparent 1', + 'grandparent 2', + ]); + }); + + it('should be called in projected components before their hosts', () => { + const events: string[] = []; + + @Component({ + selector: 'projected', + template: `

    test

    `, + }) + class Projected { + @Input() + name = ''; + + ngOnDestroy() { events.push('projected ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `
    `, + }) + class Comp { + @Input() + name = ''; + + ngOnDestroy() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` +
    + + + + + + +
    + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp, Projected], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'projected 1', + 'comp 1', + 'projected 2', + 'comp 2', + ]); + }); + + + it('should be called in consistent order if views are removed and re-added', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnDestroy() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` +
    + + + +
    + ` + }) + class App { + showAll = true; + showMiddle = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.showMiddle = false; + fixture.detectChanges(); + expect(events).toEqual(['comp 2']); + + fixture.componentInstance.showAll = false; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp 2', + 'comp 1', + 'comp 3', + ]); + + fixture.componentInstance.showAll = true; + fixture.componentInstance.showMiddle = true; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp 2', + 'comp 1', + 'comp 3', + ]); + + fixture.componentInstance.showAll = false; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp 2', + 'comp 1', + 'comp 3', + 'comp 2', + 'comp 1', + 'comp 3', + ]); + }); + + it('should be called on every iteration of a destroyed for loop', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnDestroy() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` +
    + +
    + ` + }) + class App { + show = true; + numbers = [0, 1, 2, 3]; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 0', + 'comp 1', + 'comp 2', + 'comp 3', + ]); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 0', + 'comp 1', + 'comp 2', + 'comp 3', + ]); + + fixture.componentInstance.numbers.splice(1, 1); + fixture.detectChanges(); + expect(events).toEqual([ + 'comp 0', + 'comp 1', + 'comp 2', + 'comp 3', + 'comp 1', + ]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp 0', + 'comp 1', + 'comp 2', + 'comp 3', + 'comp 1', + 'comp 0', + 'comp 2', + 'comp 3', + ]); + }); + + it('should call destroy properly if view also has listeners', () => { + const events: string[] = []; + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + ngOnDestroy() { events.push('comp'); } + } + @Component({ + template: ` +
    + + + +
    + ` + }) + class App { + show = true; + + clicksToButton1 = 0; + + clicksToButton2 = 0; + + handleClick1() { this.clicksToButton1++; } + + handleClick2() { this.clicksToButton2++; } + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const buttons = fixture.debugElement.queryAll(By.css('button')); + buttons.forEach(button => button.nativeElement.click()); + + expect(fixture.componentInstance.clicksToButton1).toBe(1); + expect(fixture.componentInstance.clicksToButton2).toBe(1); + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + buttons.forEach(button => button.nativeElement.click()); + expect(fixture.componentInstance.clicksToButton1).toBe(1); + expect(fixture.componentInstance.clicksToButton2).toBe(1); + + expect(events).toEqual(['comp']); + }); + + onlyInIvy( + 'View Engine has the opposite behavior, where it calls destroy on the directives first, then the components') + .it('should be called on directives after component', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + @Input('dir') + name = ''; + + ngOnDestroy() { events.push('dir ' + this.name); } + } + + @Component({ + selector: 'comp', + template: `

    test

    `, + }) + class Comp { + @Input() + name = ''; + + ngOnDestroy() { events.push('comp ' + this.name); } + } + + @Component({ + template: ` +
    + + +
    + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Dir, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp 1', + 'dir 1', + 'comp 2', + 'dir 2', + ]); + }); + + it('should be called on directives on an element', () => { + const events: string[] = []; + + @Directive({ + selector: '[dir]', + }) + class Dir { + ngOnDestroy() { events.push('dir'); } + } + + @Component({template: `

    `}) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Dir], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([]); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual(['dir']); + }); +}); + +describe('hook order', () => { + let events: string[] = []; + + beforeEach(() => events = []); + + @Component({ + selector: 'comp', + template: `{{value}}
    `, + }) + class Comp { + @Input() + value = ''; + + @Input() + name = ''; + + ngOnInit() { events.push(`${this.name} onInit`); } + + ngDoCheck() { events.push(`${this.name} doCheck`); } + + ngOnChanges() { events.push(`${this.name} onChanges`); } + + ngAfterContentInit() { events.push(`${this.name} afterContentInit`); } + + ngAfterContentChecked() { events.push(`${this.name} afterContentChecked`); } + + ngAfterViewInit() { events.push(`${this.name} afterViewInit`); } + + ngAfterViewChecked() { events.push(`${this.name} afterViewChecked`); } + + ngOnDestroy() { events.push(`${this.name} onDestroy`); } + } + + @Component({ + selector: 'parent', + template: + ``, + }) + class Parent extends Comp { + } + + it('should call all hooks in correct order', () => { + @Component({template: ``}) + class App { + value = 'a'; + + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'comp onChanges', + 'comp onInit', + 'comp doCheck', + 'comp afterContentInit', + 'comp afterContentChecked', + 'comp afterViewInit', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp doCheck', + 'comp afterContentChecked', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp onChanges', + 'comp doCheck', + 'comp afterContentChecked', + 'comp afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(events).toEqual([ + 'comp onDestroy', + ]); + }); + + it('should call all hooks in correct order with children', () => { + @Component({ + template: ` +
    + + +
    + ` + }) + class App { + value = 'a'; + + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 onInit', + 'parent1 doCheck', + 'parent2 onChanges', + 'parent2 onInit', + 'parent2 doCheck', + 'parent1 afterContentInit', + 'parent1 afterContentChecked', + 'parent2 afterContentInit', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 onInit', + 'child of parent1 doCheck', + 'child of parent1 afterContentInit', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewInit', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 onInit', + 'child of parent2 doCheck', + 'child of parent2 afterContentInit', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewInit', + 'child of parent2 afterViewChecked', + 'parent1 afterViewInit', + 'parent1 afterViewChecked', + 'parent2 afterViewInit', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 doCheck', + 'parent2 onChanges', + 'parent2 doCheck', + 'parent1 afterContentChecked', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 doCheck', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 doCheck', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewChecked', + 'parent1 afterViewChecked', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent1 onDestroy', + 'child of parent2 onDestroy', + 'parent1 onDestroy', + 'parent2 onDestroy', + ]); + }); + + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng + it('should call all hooks in correct order with view and content', () => { + @Component({ + template: ` +
    + + + + + + +
    + ` + }) + class App { + value = 'a'; + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, Parent, Comp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 onInit', + 'parent1 doCheck', + 'projected1 onChanges', + 'projected1 onInit', + 'projected1 doCheck', + 'parent2 onChanges', + 'parent2 onInit', + 'parent2 doCheck', + 'projected2 onChanges', + 'projected2 onInit', + 'projected2 doCheck', + 'projected1 afterContentInit', + 'projected1 afterContentChecked', + 'parent1 afterContentInit', + 'parent1 afterContentChecked', + 'projected2 afterContentInit', + 'projected2 afterContentChecked', + 'parent2 afterContentInit', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 onInit', + 'child of parent1 doCheck', + 'child of parent1 afterContentInit', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewInit', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 onInit', + 'child of parent2 doCheck', + 'child of parent2 afterContentInit', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewInit', + 'child of parent2 afterViewChecked', + 'projected1 afterViewInit', + 'projected1 afterViewChecked', + 'parent1 afterViewInit', + 'parent1 afterViewChecked', + 'projected2 afterViewInit', + 'projected2 afterViewChecked', + 'parent2 afterViewInit', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.value = 'b'; + fixture.detectChanges(); + + expect(events).toEqual([ + 'parent1 onChanges', + 'parent1 doCheck', + 'projected1 onChanges', + 'projected1 doCheck', + 'parent2 onChanges', + 'parent2 doCheck', + 'projected2 onChanges', + 'projected2 doCheck', + 'projected1 afterContentChecked', + 'parent1 afterContentChecked', + 'projected2 afterContentChecked', + 'parent2 afterContentChecked', + 'child of parent1 onChanges', + 'child of parent1 doCheck', + 'child of parent1 afterContentChecked', + 'child of parent1 afterViewChecked', + 'child of parent2 onChanges', + 'child of parent2 doCheck', + 'child of parent2 afterContentChecked', + 'child of parent2 afterViewChecked', + 'projected1 afterViewChecked', + 'parent1 afterViewChecked', + 'projected2 afterViewChecked', + 'parent2 afterViewChecked', + ]); + + events.length = 0; + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(events).toEqual([ + 'child of parent1 onDestroy', + 'child of parent2 onDestroy', + 'projected1 onDestroy', + 'parent1 onDestroy', + 'projected2 onDestroy', + 'parent2 onDestroy', + ]); + }); +}); + +describe('non-regression', () => { + it('should call lifecycle hooks for directives active on ', () => { + let destroyed = false; + + @Directive({ + selector: '[onDestroyDir]', + }) + class OnDestroyDir { + ngOnDestroy() { destroyed = true; } + } + + @Component({ + template: ` + content + ` + }) + class App { + show = true; + } + + TestBed.configureTestingModule({ + declarations: [App, OnDestroyDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(destroyed).toBeFalsy(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(destroyed).toBeTruthy(); + }); +}); diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts index 044520b060..52dfca3b00 100644 --- a/packages/core/test/acceptance/listener_spec.ts +++ b/packages/core/test/acceptance/listener_spec.ts @@ -220,7 +220,7 @@ describe('event listeners', () => { count = 0; someValue = -1; - @ViewChild(FooDirective) fooDirective: FooDirective|null = null; + @ViewChild(FooDirective, {static: false}) fooDirective: FooDirective|null = null; fooChange() { this.count++; } diff --git a/packages/core/test/acceptance/outputs_spec.ts b/packages/core/test/acceptance/outputs_spec.ts index 873b663388..35716f1625 100644 --- a/packages/core/test/acceptance/outputs_spec.ts +++ b/packages/core/test/acceptance/outputs_spec.ts @@ -43,7 +43,7 @@ describe('outputs', () => { @Component({template: ''}) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; onChange() { counter++; } } TestBed.configureTestingModule({declarations: [App, ButtonToggle]}); @@ -64,7 +64,7 @@ describe('outputs', () => { @Component( {template: ''}) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; onChange() { counter++; } onReset() { resetCounter++; } } @@ -82,7 +82,7 @@ describe('outputs', () => { it('should eval component output expression when event is emitted', () => { @Component({template: ''}) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; counter = 0; } TestBed.configureTestingModule({declarations: [App, ButtonToggle]}); @@ -102,7 +102,7 @@ describe('outputs', () => { @Component( {template: ''}) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; condition = true; onChange() { counter++; } @@ -133,7 +133,7 @@ describe('outputs', () => { ` }) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; condition = true; condition2 = true; @@ -168,8 +168,8 @@ describe('outputs', () => { ` }) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; - @ViewChild(DestroyComp) destroyComp !: DestroyComp; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; + @ViewChild(DestroyComp, {static: false}) destroyComp !: DestroyComp; condition = true; onClick() { clickCounter++; } @@ -206,7 +206,7 @@ describe('outputs', () => { @Component({template: ''}) class App { - @ViewChild(MyButton) buttonDir !: MyButton; + @ViewChild(MyButton, {static: false}) buttonDir !: MyButton; onClick() { counter++; } } TestBed.configureTestingModule({declarations: [App, MyButton]}); @@ -228,8 +228,8 @@ describe('outputs', () => { @Component({template: ''}) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; - @ViewChild(OtherDir) otherDir !: OtherDir; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; + @ViewChild(OtherDir, {static: false}) otherDir !: OtherDir; onChange() { counter++; } } TestBed.configureTestingModule({declarations: [App, ButtonToggle, OtherDir]}); @@ -257,8 +257,8 @@ describe('outputs', () => { '' }) class App { - @ViewChild(ButtonToggle) buttonToggle !: ButtonToggle; - @ViewChild(OtherChangeDir) otherDir !: OtherChangeDir; + @ViewChild(ButtonToggle, {static: false}) buttonToggle !: ButtonToggle; + @ViewChild(OtherChangeDir, {static: false}) otherDir !: OtherChangeDir; change = true; onChange() { counter++; } diff --git a/packages/core/test/acceptance/pipe_spec.ts b/packages/core/test/acceptance/pipe_spec.ts index a5b5c8c48e..56efdd827c 100644 --- a/packages/core/test/acceptance/pipe_spec.ts +++ b/packages/core/test/acceptance/pipe_spec.ts @@ -70,7 +70,7 @@ describe('pipe', () => { template: `
    `, }) class App { - @ViewChild(Dir) directive !: Dir; + @ViewChild(Dir, {static: false}) directive !: Dir; } TestBed.configureTestingModule({declarations: [App, DoublePipe, Dir]}); diff --git a/packages/core/test/acceptance/property_binding_spec.ts b/packages/core/test/acceptance/property_binding_spec.ts index 1bfe65e1b7..8c96cfed80 100644 --- a/packages/core/test/acceptance/property_binding_spec.ts +++ b/packages/core/test/acceptance/property_binding_spec.ts @@ -5,11 +5,30 @@ * 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 {Component, Input} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, EventEmitter, Input, Output} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By, DomSanitizer, SafeUrl} from '@angular/platform-browser'; describe('property bindings', () => { + it('should support bindings to properties', () => { + @Component({template: ``}) + class Comp { + id: string|undefined; + } + + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const spanEl = fixture.nativeElement.querySelector('span'); + + expect(spanEl.id).toBeFalsy(); + + fixture.componentInstance.id = 'testId'; + fixture.detectChanges(); + + expect(spanEl.id).toBe('testId'); + }); + it('should update bindings when value changes', () => { @Component({ template: ``, @@ -135,4 +154,448 @@ describe('property bindings', () => { expect(fixture.debugElement.query(By.css('input')).nativeElement.required).toBe(false); }); + + it('should support interpolation for properties', () => { + @Component({template: ``}) + class Comp { + id: string|undefined; + } + + TestBed.configureTestingModule({declarations: [Comp]}); + const fixture = TestBed.createComponent(Comp); + const spanEl = fixture.nativeElement.querySelector('span'); + + fixture.componentInstance.id = 'testId'; + fixture.detectChanges(); + expect(spanEl.id).toBe('_testId_'); + + fixture.componentInstance.id = 'otherId'; + fixture.detectChanges(); + expect(spanEl.id).toBe('_otherId_'); + }); + + describe('input properties', () => { + @Directive({ + selector: '[myButton]', + }) + class MyButton { + @Input() disabled: boolean|undefined; + } + + @Directive({ + selector: '[otherDir]', + }) + class OtherDir { + @Input() id: number|undefined; + @Output('click') clickStream = new EventEmitter(); + } + + @Directive({ + selector: '[otherDisabledDir]', + }) + class OtherDisabledDir { + @Input() disabled: boolean|undefined; + } + + @Directive({ + selector: '[idDir]', + }) + class IdDir { + @Input('id') idNumber: string|undefined; + } + + it('should check input properties before setting (directives)', () => { + @Component({ + template: `` + }) + class App { + id = 0; + isDisabled = true; + } + + TestBed.configureTestingModule({declarations: [App, MyButton, OtherDir]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('mybutton')).toBe(''); + expect(buttonEl.getAttribute('otherdir')).toBe(''); + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + expect(otherDir.id).toEqual(0); + + fixture.componentInstance.isDisabled = false; + fixture.componentInstance.id = 1; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('mybutton')).toBe(''); + expect(buttonEl.getAttribute('otherdir')).toBe(''); + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + expect(otherDir.id).toEqual(1); + }); + + it('should support mixed element properties and input properties', () => { + @Component({template: ``}) + class App { + isDisabled = true; + id = 0; + } + + TestBed.configureTestingModule({declarations: [App, MyButton]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('id')).toBe('0'); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + + fixture.componentInstance.isDisabled = false; + fixture.componentInstance.id = 1; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('id')).toBe('1'); + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + }); + + it('should check that property is not an input property before setting (component)', () => { + @Component({ + selector: 'comp', + template: '', + }) + class Comp { + @Input() id: number|undefined; + } + + @Component({template: ``}) + class App { + id = 1; + } + + TestBed.configureTestingModule({declarations: [App, Comp]}); + const fixture = TestBed.createComponent(App); + const compDebugEl = fixture.debugElement.query(By.directive(Comp)); + fixture.detectChanges(); + + expect(compDebugEl.nativeElement.hasAttribute('id')).toBe(false); + expect(compDebugEl.componentInstance.id).toEqual(1); + + fixture.componentInstance.id = 2; + fixture.detectChanges(); + + expect(compDebugEl.nativeElement.hasAttribute('id')).toBe(false); + expect(compDebugEl.componentInstance.id).toEqual(2); + }); + + it('should support two input properties with the same name', () => { + @Component( + {template: ``}) + class App { + isDisabled = true; + } + + TestBed.configureTestingModule({declarations: [App, MyButton, OtherDisabledDir]}); + const fixture = TestBed.createComponent(App); + const button = fixture.debugElement.query(By.directive(MyButton)).injector.get(MyButton); + const otherDisabledDir = + fixture.debugElement.query(By.directive(OtherDisabledDir)).injector.get(OtherDisabledDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(true); + expect(otherDisabledDir.disabled).toEqual(true); + + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('disabled')).toBe(false); + expect(button.disabled).toEqual(false); + expect(otherDisabledDir.disabled).toEqual(false); + }); + + it('should set input property if there is an output first', () => { + @Component({ + template: ``, + }) + class App { + id = 1; + counter = 0; + onClick = () => this.counter++; + } + + TestBed.configureTestingModule({declarations: [App, OtherDir]}); + const fixture = TestBed.createComponent(App); + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + const buttonEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(buttonEl.hasAttribute('id')).toBe(false); + expect(otherDir.id).toEqual(1); + + otherDir.clickStream.next(); + expect(fixture.componentInstance.counter).toEqual(1); + + fixture.componentInstance.id = 2; + fixture.detectChanges(); + expect(otherDir.id).toEqual(2); + }); + + it('should support unrelated element properties at same index in if-else block', () => { + @Component({ + template: ` + + + + ` + }) + class App { + condition = true; + id1 = 'one'; + id2 = 'two'; + id3 = 3; + } + + TestBed.configureTestingModule( + {declarations: [App, IdDir, OtherDir], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + let buttonElements = fixture.nativeElement.querySelectorAll('button'); + const idDir = fixture.debugElement.query(By.directive(IdDir)).injector.get(IdDir); + + expect(buttonElements.length).toBe(2); + expect(buttonElements[0].hasAttribute('id')).toBe(false); + expect(buttonElements[1].getAttribute('id')).toBe('two'); + expect(buttonElements[1].textContent).toBe('Click me too (2)'); + expect(idDir.idNumber).toBe('one'); + + fixture.componentInstance.condition = false; + fixture.componentInstance.id1 = 'four'; + fixture.detectChanges(); + + const otherDir = fixture.debugElement.query(By.directive(OtherDir)).injector.get(OtherDir); + buttonElements = fixture.nativeElement.querySelectorAll('button'); + expect(buttonElements.length).toBe(2); + expect(buttonElements[0].hasAttribute('id')).toBe(false); + expect(buttonElements[1].hasAttribute('id')).toBe(false); + expect(buttonElements[1].textContent).toBe('Click me too (3)'); + expect(idDir.idNumber).toBe('four'); + expect(otherDir.id).toBe(3); + }); + }); + + describe('attributes and input properties', () => { + + @Directive({selector: '[myDir]', exportAs: 'myDir'}) + class MyDir { + @Input() role: string|undefined; + @Input('dir') direction: string|undefined; + @Output('change') changeStream = new EventEmitter(); + } + + @Directive({selector: '[myDirB]'}) + class MyDirB { + @Input('role') roleB: string|undefined; + } + + it('should set input property based on attribute if existing', () => { + @Component({template: `
    `}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(divElement.getAttribute('mydir')).toBe(''); + expect(myDir.role).toEqual('button'); + }); + + it('should set input property and attribute if both defined', () => { + @Component({template: `
    `}) + class App { + role = 'listbox'; + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('listbox'); + + fixture.componentInstance.role = 'button'; + fixture.detectChanges(); + expect(myDir.role).toEqual('button'); + }); + + it('should set two directive input properties based on same attribute', () => { + @Component({template: `
    `}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('button'); + expect(myDirB.roleB).toEqual('button'); + }); + + it('should process two attributes on same directive', () => { + @Component({ + template: `
    `, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(divElement.getAttribute('dir')).toBe('rtl'); + expect(myDir.role).toEqual('button'); + expect(myDir.direction).toEqual('rtl'); + }); + + it('should process attributes and outputs properly together', () => { + @Component({template: `
    `}) + class App { + counter = 0; + onChange = () => this.counter++; + } + + TestBed.configureTestingModule({declarations: [App, MyDir]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const divElement = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + expect(divElement.getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('button'); + + myDir.changeStream.next(); + expect(fixture.componentInstance.counter).toEqual(1); + }); + + it('should process attributes properly for directives with later indices', () => { + @Component({ + template: ` +
    +
    + `, + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB]}); + const fixture = TestBed.createComponent(App); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + const [buttonEl, listboxEl] = fixture.nativeElement.children; + fixture.detectChanges(); + + expect(buttonEl.getAttribute('role')).toBe('button'); + expect(buttonEl.getAttribute('dir')).toBe('rtl'); + expect(listboxEl.getAttribute('role')).toBe('listbox'); + + expect(myDir.role).toEqual('button'); + expect(myDir.direction).toEqual('rtl'); + expect(myDirB.roleB).toEqual('listbox'); + }); + + it('should support attributes at same index inside an if-else block', () => { + @Component({ + template: ` +
    +
    +
    + `, + }) + class App { + condition = true; + } + + TestBed.configureTestingModule({declarations: [App, MyDir, MyDirB], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myDir = fixture.debugElement.query(By.directive(MyDir)).injector.get(MyDir); + const myDirB = fixture.debugElement.query(By.directive(MyDirB)).injector.get(MyDirB); + let divElements = fixture.nativeElement.querySelectorAll('div'); + + expect(divElements.length).toBe(2); + expect(divElements[0].getAttribute('role')).toBe('listbox'); + expect(divElements[1].getAttribute('role')).toBe('button'); + expect(myDir.role).toEqual('listbox'); + expect(myDirB.roleB).toEqual('button'); + expect((myDirB as any).role).toBeUndefined(); + + fixture.componentInstance.condition = false; + fixture.detectChanges(); + + divElements = fixture.nativeElement.querySelectorAll('div'); + expect(divElements.length).toBe(2); + expect(divElements[0].getAttribute('role')).toBe('listbox'); + expect(divElements[1].getAttribute('role')).toBe('menu'); + expect(myDir.role).toEqual('listbox'); + expect(myDirB.roleB).toEqual('button'); + }); + + it('should process attributes properly inside a for loop', () => { + @Component({ + selector: 'comp', + template: `
    role: {{dir.role}}` + }) + class Comp { + } + + @Component({ + template: ` + + ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyDir, Comp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.children.length).toBe(2); + + const [comp1, comp2] = fixture.nativeElement.children; + + expect(comp1.tagName).toBe('COMP'); + expect(comp2.tagName).toBe('COMP'); + + expect(comp1.children[0].tagName).toBe('DIV'); + expect(comp1.children[0].getAttribute('role')).toBe('button'); + expect(comp1.textContent).toBe('role: button'); + + expect(comp2.children[0].tagName).toBe('DIV'); + expect(comp2.children[0].getAttribute('role')).toBe('button'); + expect(comp2.textContent).toBe('role: button'); + }); + + }); + }); diff --git a/packages/core/test/acceptance/pure_function_spec.ts b/packages/core/test/acceptance/pure_function_spec.ts new file mode 100644 index 0000000000..01abb54b2e --- /dev/null +++ b/packages/core/test/acceptance/pure_function_spec.ts @@ -0,0 +1,482 @@ +/** + * @license + * Copyright Google Inc. 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 {CommonModule} from '@angular/common'; +import {Component, Input} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +describe('components using pure function instructions internally', () => { + describe('with array literals', () => { + @Component({ + selector: 'my-comp', + template: ``, + }) + class MyComp { + @Input() + names: string[] = []; + } + + + it('should support an array literal with a binding', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + + const firstArray = myComp.names; + expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess']); + + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess']); + expect(firstArray).toBe(myComp.names); + + fixture.componentInstance.customName = 'Hannah'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Hannah', 'Bess']); + + // Identity must change if binding changes + expect(firstArray).not.toBe(myComp.names); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + myComp.names = ['should not be overwritten']; + fixture.detectChanges(); + + expect(myComp !.names).toEqual(['should not be overwritten']); + }); + + + it('should support array literals in dynamic views', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess']); + }); + + it('should support multiple array literals passed through to one node', () => { + @Component({ + selector: 'many-prop-comp', + template: ``, + }) + class ManyPropComp { + @Input() + names1: string[] = []; + + @Input() + names2: string[] = []; + } + + @Component({ + template: ` + + + `, + }) + class App { + showing = true; + customName = 'Carson'; + customName2 = 'George'; + } + + TestBed.configureTestingModule({ + declarations: [App, ManyPropComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const manyPropComp = fixture.debugElement.query(By.directive(ManyPropComp)).componentInstance; + + expect(manyPropComp !.names1).toEqual(['Nancy', 'Carson']); + expect(manyPropComp !.names2).toEqual(['George']); + + fixture.componentInstance.customName = 'George'; + fixture.componentInstance.customName2 = 'Carson'; + fixture.detectChanges(); + expect(manyPropComp !.names1).toEqual(['Nancy', 'George']); + expect(manyPropComp !.names2).toEqual(['Carson']); + }); + + + it('should support an array literals inside fn calls', () => { + @Component({ + selector: 'parent-comp', + template: ` + + ` + }) + class ParentComp { + customName = 'Bess'; + + someFn(arr: string[]): string[] { + arr[0] = arr[0].toUpperCase(); + return arr; + } + } + + @Component({ + template: ` + + + ` + }) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp, ParentComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComps = + fixture.debugElement.queryAll(By.directive(MyComp)).map(f => f.componentInstance); + + + const firstArray = myComps[0].names; + const secondArray = myComps[1].names; + expect(firstArray).toEqual(['NANCY', 'Bess']); + expect(secondArray).toEqual(['NANCY', 'Bess']); + expect(firstArray).not.toBe(secondArray); + + fixture.detectChanges(); + expect(firstArray).toEqual(['NANCY', 'Bess']); + expect(secondArray).toEqual(['NANCY', 'Bess']); + expect(firstArray).toBe(myComps[0].names); + expect(secondArray).toBe(myComps[1].names); + }); + + + it('should support an array literal with more than 1 binding', () => { + @Component({ + template: ` + + `, + }) + class App { + showing = true; + customName = 'Carson'; + customName2 = 'Hannah'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + imports: [CommonModule], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + + const firstArray = myComp.names; + expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); + + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); + expect(firstArray).toBe(myComp.names); + + fixture.componentInstance.customName = 'George'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'George', 'Bess', 'Hannah']); + expect(firstArray).not.toBe(myComp.names); + + fixture.componentInstance.customName = 'Frank'; + fixture.componentInstance.customName2 = 'Ned'; + fixture.detectChanges(); + expect(myComp.names).toEqual(['Nancy', 'Frank', 'Bess', 'Ned']); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + myComp.names = ['should not be overwritten']; + fixture.detectChanges(); + expect(myComp.names).toEqual(['should not be overwritten']); + }); + + + it('should work up to 8 bindings', () => { + + @Component({ + template: ` + + + + + + + + + ` + }) + class App { + v1 = 'a'; + v2 = 'b'; + v3 = 'c'; + v4 = 'd'; + v5 = 'e'; + v6 = 'f'; + v7 = 'g'; + v8 = 'h'; + } + + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const myComps = + fixture.debugElement.queryAll(By.directive(MyComp)).map(f => f.componentInstance); + + myComps.forEach(myComp => { + expect(myComp.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); + }); + + const app = fixture.componentInstance; + + app.v1 = 'a1'; + app.v2 = 'b1'; + app.v3 = 'c1'; + app.v4 = 'd1'; + app.v5 = 'e1'; + app.v6 = 'f1'; + app.v7 = 'g1'; + app.v8 = 'h1'; + + fixture.detectChanges(); + + expect(myComps[0].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h1']); + expect(myComps[1].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'h1']); + expect(myComps[2].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h1']); + expect(myComps[3].names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[4].names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[5].names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[6].names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + expect(myComps[7].names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); + + app.v8 = 'h2'; + fixture.detectChanges(); + + expect(myComps[0].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h2']); + expect(myComps[1].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'h2']); + expect(myComps[2].names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h2']); + expect(myComps[3].names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[4].names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[5].names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[6].names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + expect(myComps[7].names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); + }); + + it('should work with pureFunctionV for 9+ bindings', () => { + @Component({ + template: ` + + + ` + }) + class App { + v0 = 'a'; + v1 = 'b'; + v2 = 'c'; + v3 = 'd'; + v4 = 'e'; + v5 = 'f'; + v6 = 'g'; + v7 = 'h'; + v8 = 'i'; + } + TestBed.configureTestingModule({ + declarations: [App, MyComp], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const myComp = fixture.debugElement.query(By.directive(MyComp)).componentInstance; + const app = fixture.componentInstance; + + expect(myComp.names).toEqual([ + 'start', 'a', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' + ]); + + app.v0 = 'a1'; + fixture.detectChanges(); + + expect(myComp.names).toEqual([ + 'start', 'a1', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' + ]); + + app.v4 = 'e5'; + fixture.detectChanges(); + + expect(myComp.names).toEqual([ + 'start', 'a1', 'b', 'c', 'd', 'modified_e5', 'f', 'g', 'h', 'i', 'end' + ]); + }); + }); + + describe('with object literals', () => { + @Component({ + selector: 'object-comp', + template: ``, + }) + class ObjectComp { + @Input() + config: any = []; + } + + it('should support an object literal', () => { + @Component({ + template: '', + }) + class App { + name = 'slide'; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const objectComp = fixture.debugElement.query(By.directive(ObjectComp)).componentInstance; + + const firstObj = objectComp.config; + expect(objectComp.config).toEqual({duration: 500, animation: 'slide'}); + + fixture.detectChanges(); + expect(objectComp.config).toEqual({duration: 500, animation: 'slide'}); + expect(firstObj).toBe(objectComp.config); + + fixture.componentInstance.name = 'tap'; + fixture.detectChanges(); + expect(objectComp.config).toEqual({duration: 500, animation: 'tap'}); + + // Identity must change if binding changes + expect(firstObj).not.toBe(objectComp.config); + }); + + + it('should support expressions nested deeply in object/array literals', () => { + @Component({ + template: ` + + + `, + }) + class App { + name = 'slide'; + duration = 100; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const objectComp = fixture.debugElement.query(By.directive(ObjectComp)).componentInstance; + + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + const firstConfig = objectComp.config; + + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + expect(objectComp.config).toBe(firstConfig); + + fixture.componentInstance.duration = 50; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + expect(objectComp.config).not.toBe(firstConfig); + + fixture.componentInstance.name = 'tap'; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'tap', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + + fixture.componentInstance.name = 'drag'; + fixture.componentInstance.duration = 500; + fixture.detectChanges(); + expect(objectComp.config).toEqual({ + animation: 'drag', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 500}] + }); + + // The property should not be set if the exp value is the same, so artificially + // setting the property to ensure it's not overwritten. + objectComp.config = ['should not be overwritten']; + fixture.detectChanges(); + expect(objectComp.config).toEqual(['should not be overwritten']); + }); + + it('should support multiple view instances with multiple bindings', () => { + @Component({ + template: ` + + + `, + }) + class App { + configs = [ + {opacity: 0, duration: 500}, + {opacity: 1, duration: 600}, + ]; + } + + TestBed.configureTestingModule({ + declarations: [App, ObjectComp], + imports: [CommonModule], + }); + + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const app = fixture.componentInstance; + const objectComps = + fixture.debugElement.queryAll(By.directive(ObjectComp)).map(f => f.componentInstance); + + expect(objectComps[0].config).toEqual({opacity: 0, duration: 500}); + expect(objectComps[1].config).toEqual({opacity: 1, duration: 600}); + + app.configs[0].duration = 1000; + fixture.detectChanges(); + expect(objectComps[0].config).toEqual({opacity: 0, duration: 1000}); + expect(objectComps[1].config).toEqual({opacity: 1, duration: 600}); + }); + }); +}); diff --git a/packages/core/test/acceptance/query_spec.ts b/packages/core/test/acceptance/query_spec.ts index 3a260bfce7..12a75203a1 100644 --- a/packages/core/test/acceptance/query_spec.ts +++ b/packages/core/test/acceptance/query_spec.ts @@ -6,8 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, ContentChild, ContentChildren, Directive, ElementRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; @@ -175,7 +177,7 @@ describe('query logic', () => { it('should support ViewChild query inherited from undecorated superclasses', () => { class MyComp { - @ViewChild('foo') foo: any; + @ViewChild('foo', {static: false}) foo: any; } @Component({selector: 'sub-comp', template: '
    '}) @@ -191,7 +193,7 @@ describe('query logic', () => { it('should support ViewChild query inherited from undecorated grand superclasses', () => { class MySuperComp { - @ViewChild('foo') foo: any; + @ViewChild('foo', {static: false}) foo: any; } class MyComp extends MySuperComp {} @@ -491,7 +493,7 @@ describe('query logic', () => { it('should support ContentChild query inherited from undecorated superclasses', () => { class MyComp { - @ContentChild('foo') foo: any; + @ContentChild('foo', {static: false}) foo: any; } @Component({selector: 'sub-comp', template: ''}) @@ -500,7 +502,7 @@ describe('query logic', () => { @Component({template: '
    '}) class App { - @ViewChild(SubComp) subComp !: SubComp; + @ViewChild(SubComp, {static: false}) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); @@ -512,7 +514,7 @@ describe('query logic', () => { it('should support ContentChild query inherited from undecorated grand superclasses', () => { class MySuperComp { - @ContentChild('foo') foo: any; + @ContentChild('foo', {static: false}) foo: any; } class MyComp extends MySuperComp {} @@ -523,7 +525,7 @@ describe('query logic', () => { @Component({template: '
    '}) class App { - @ViewChild(SubComp) subComp !: SubComp; + @ViewChild(SubComp, {static: false}) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp]}); @@ -555,7 +557,7 @@ describe('query logic', () => { ` }) class App { - @ViewChild(SubComp) subComp !: SubComp; + @ViewChild(SubComp, {static: false}) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); @@ -590,7 +592,7 @@ describe('query logic', () => { ` }) class App { - @ViewChild(SubComp) subComp !: SubComp; + @ViewChild(SubComp, {static: false}) subComp !: SubComp; } TestBed.configureTestingModule({declarations: [App, SubComp, SomeDir]}); @@ -601,6 +603,44 @@ describe('query logic', () => { expect(fixture.componentInstance.subComp.foo.length).toBe(2); }); + it('should match shallow content queries in views inserted / removed by ngIf', () => { + @Component({ + selector: 'test-comp', + template: ` + +
    +
    + ` + }) + class TestComponent { + showing = false; + } + + @Component({ + selector: 'shallow-comp', + template: '', + }) + class ShallowComp { + @ContentChildren('foo', {descendants: false}) foos !: QueryList; + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, ShallowComp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const shallowComp = fixture.debugElement.query(By.directive(ShallowComp)).componentInstance; + const queryList = shallowComp !.foos; + expect(queryList.length).toBe(0); + + fixture.componentInstance.showing = true; + fixture.detectChanges(); + expect(queryList.length).toBe(1); + + fixture.componentInstance.showing = false; + fixture.detectChanges(); + expect(queryList.length).toBe(0); + }); }); // Some root components may have ContentChildren queries if they are also @@ -665,6 +705,254 @@ describe('query logic', () => { }); + describe('view boundaries', () => { + + describe('ViewContainerRef', () => { + + @Directive({selector: '[vc]', exportAs: 'vc'}) + class ViewContainerManipulatorDirective { + constructor(private _vcRef: ViewContainerRef) {} + + insertTpl(tpl: TemplateRef<{}>, ctx: {}, idx?: number) { + this._vcRef.createEmbeddedView(tpl, ctx, idx); + } + + remove(index?: number) { this._vcRef.remove(index); } + } + + it('should report results in views inserted / removed by ngIf', () => { + @Component({ + selector: 'test-comp', + template: ` + +
    +
    + ` + }) + class TestComponent { + value: boolean = false; + @ViewChildren('foo') query !: QueryList; + } + + TestBed.configureTestingModule({declarations: [TestComponent]}); + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const queryList = fixture.componentInstance.query; + expect(queryList.length).toBe(0); + + fixture.componentInstance.value = true; + fixture.detectChanges(); + expect(queryList.length).toBe(1); + + fixture.componentInstance.value = false; + fixture.detectChanges(); + expect(queryList.length).toBe(0); + }); + + it('should report results in views inserted / removed by ngFor', () => { + @Component({ + selector: 'test-comp', + template: ` + +
    +
    + `, + }) + class TestComponent { + value: string[]|undefined; + @ViewChildren('foo') query !: QueryList; + } + + TestBed.configureTestingModule({declarations: [TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const queryList = fixture.componentInstance.query; + expect(queryList.length).toBe(0); + + fixture.componentInstance.value = ['a', 'b', 'c']; + fixture.detectChanges(); + expect(queryList.length).toBe(3); + + // Remove the "b" element from the value. + fixture.componentInstance.value.splice(1, 1); + fixture.detectChanges(); + expect(queryList.length).toBe(2); + + // make sure that the "b" element has been removed from query results + expect(queryList.first.nativeElement.id).toBe('a'); + expect(queryList.last.nativeElement.id).toBe('c'); + }); + + // https://stackblitz.com/edit/angular-rrmmuf?file=src/app/app.component.ts + it('should report results when different instances of TemplateRef are inserted into one ViewContainerRefs', + () => { + @Component({ + selector: 'test-comp', + template: ` + +
    +
    + +
    + + +
    +
    + + + `, + }) + class TestComponent { + @ViewChild(ViewContainerManipulatorDirective, {static: false}) + vc !: ViewContainerManipulatorDirective; + @ViewChild('tpl1', {static: false}) tpl1 !: TemplateRef; + @ViewChild('tpl2', {static: false}) tpl2 !: TemplateRef; + @ViewChildren('foo') query !: QueryList; + } + + TestBed.configureTestingModule( + {declarations: [ViewContainerManipulatorDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const queryList = fixture.componentInstance.query; + const {tpl1, tpl2, vc} = fixture.componentInstance; + + expect(queryList.length).toBe(1); + expect(queryList.first.nativeElement.getAttribute('id')).toBe('middle'); + + vc.insertTpl(tpl1 !, {idx: 0}, 0); + vc.insertTpl(tpl2 !, {idx: 1}, 1); + fixture.detectChanges(); + + expect(queryList.length).toBe(3); + let qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); + expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); + + vc.insertTpl(tpl1 !, {idx: 1}, 1); + fixture.detectChanges(); + + expect(queryList.length).toBe(4); + qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo1_1'); + expect(qListArr[2].nativeElement.getAttribute('id')).toBe('middle'); + expect(qListArr[3].nativeElement.getAttribute('id')).toBe('foo2_1'); + + vc.remove(1); + fixture.detectChanges(); + + expect(queryList.length).toBe(3); + qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); + expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); + + vc.remove(1); + fixture.detectChanges(); + + expect(queryList.length).toBe(2); + qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); + }); + + // https://stackblitz.com/edit/angular-7vvo9j?file=src%2Fapp%2Fapp.component.ts + // https://stackblitz.com/edit/angular-xzwp6n + onlyInIvy('FW-1318: QueryList entries are ordered differently in Ivy.') + .it('should report results when the same TemplateRef is inserted into different ViewContainerRefs', + () => { + @Component({ + selector: 'test-comp', + template: ` + +
    +
    + + + + `, + }) + class TestComponent { + @ViewChild('tpl', {static: false}) tpl !: TemplateRef; + @ViewChild('vi0', {static: false}) vi0 !: ViewContainerManipulatorDirective; + @ViewChild('vi1', {static: false}) vi1 !: ViewContainerManipulatorDirective; + @ViewChildren('foo') query !: QueryList; + } + + TestBed.configureTestingModule( + {declarations: [ViewContainerManipulatorDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const queryList = fixture.componentInstance.query; + const {tpl, vi0, vi1} = fixture.componentInstance; + + expect(queryList.length).toBe(0); + + vi0.insertTpl(tpl !, {idx: 0, container_idx: 0}, 0); + vi1.insertTpl(tpl !, {idx: 0, container_idx: 1}, 0); + fixture.detectChanges(); + + expect(queryList.length).toBe(2); + let qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); + expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo_0_0'); + + vi0.remove(); + fixture.detectChanges(); + + expect(queryList.length).toBe(1); + qListArr = queryList.toArray(); + expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); + + vi1.remove(); + fixture.detectChanges(); + expect(queryList.length).toBe(0); + }); + + // https://stackblitz.com/edit/angular-wpd6gv?file=src%2Fapp%2Fapp.component.ts + it('should report results from views inserted in a lifecycle hook', () => { + @Component({ + selector: 'my-app', + template: ` + + + + + + `, + }) + class MyApp { + show = false; + @ViewChildren('foo') query !: QueryList; + } + + TestBed.configureTestingModule({declarations: [MyApp], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const queryList = fixture.componentInstance.query; + expect(queryList.length).toBe(0); + + fixture.componentInstance.show = true; + fixture.detectChanges(); + expect(queryList.length).toBe(1); + expect(queryList.first.nativeElement.id).toBe('from_tpl'); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(queryList.length).toBe(0); + }); + + }); + }); + }); function initWithTemplate(compType: Type, template: string) { @@ -676,8 +964,8 @@ function initWithTemplate(compType: Type, template: string) { @Component({selector: 'local-ref-query-component', template: ''}) class QueryComp { - @ViewChild('viewQuery') viewChild !: any; - @ContentChild('contentQuery') contentChild !: any; + @ViewChild('viewQuery', {static: false}) viewChild !: any; + @ContentChild('contentQuery', {static: false}) contentChild !: any; @ViewChildren('viewQuery') viewChildren !: QueryList; @ContentChildren('contentQuery') contentChildren !: QueryList; diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts new file mode 100644 index 0000000000..be26ca3543 --- /dev/null +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -0,0 +1,200 @@ +/** + * @license + * Copyright Google Inc. 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 {AnimationEvent} from '@angular/animations'; +import {ɵAnimationEngine, ɵNoopAnimationStyleNormalizer} from '@angular/animations/browser'; +import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; +import {DOCUMENT} from '@angular/common'; +import {Component, DoCheck, NgZone, RendererFactory2, RendererType2} from '@angular/core'; +import {NoopNgZone} from '@angular/core/src/zone/ng_zone'; +import {TestBed} from '@angular/core/testing'; +import {EventManager, ɵDomSharedStylesHost} from '@angular/platform-browser'; +import {ɵAnimationRendererFactory} from '@angular/platform-browser/animations'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {ServerRendererFactory2} from '@angular/platform-server/src/server_renderer'; +import {onlyInIvy} from '@angular/private/testing'; + +describe('renderer factory lifecycle', () => { + let logs: string[] = []; + + @Component({selector: 'some-component', template: `foo`}) + class SomeComponent implements DoCheck { + ngOnInit() { logs.push('some_component create'); } + ngDoCheck() { logs.push('some_component update'); } + } + + @Component({selector: 'some-component-with-error', template: `With error`}) + class SomeComponentWhichThrows { + ngOnInit() { throw new Error('SomeComponentWhichThrows threw'); } + } + + @Component({selector: 'lol', template: ``}) + class TestComponent implements DoCheck { + ngOnInit() { logs.push('test_component create'); } + ngDoCheck() { logs.push('test_component update'); } + } + + /** Creates a patched renderer factory that pushes entries to the test log */ + function createPatchedRendererFactory(document: any) { + let rendererFactory = getRendererFactory2(document); + const createRender = rendererFactory.createRenderer; + + rendererFactory.createRenderer = (hostElement: any, type: RendererType2 | null) => { + logs.push('create'); + return createRender.apply(rendererFactory, [hostElement, type]); + }; + + rendererFactory.begin = () => logs.push('begin'); + rendererFactory.end = () => logs.push('end'); + + return rendererFactory; + } + + beforeEach(() => { + logs = []; + + TestBed.configureTestingModule({ + declarations: [SomeComponent, SomeComponentWhichThrows, TestComponent], + providers: [{ + provide: RendererFactory2, + useFactory: (document: any) => createPatchedRendererFactory(document), + deps: [DOCUMENT] + }] + }); + }); + + onlyInIvy('FW-1320: Ivy creates renderer twice.').it('should work with a component', () => { + const fixture = TestBed.createComponent(SomeComponent); + fixture.detectChanges(); + expect(logs).toEqual( + ['create', 'create', 'begin', 'some_component create', 'some_component update', 'end']); + + logs = []; + fixture.detectChanges(); + expect(logs).toEqual(['begin', 'some_component update', 'end']); + }); + + onlyInIvy('FW-1320: Ivy creates renderer twice.') + .it('should work with a component which throws', () => { + expect(() => { + const fixture = TestBed.createComponent(SomeComponentWhichThrows); + fixture.detectChanges(); + }).toThrow(); + expect(logs).toEqual(['create', 'create', 'begin', 'end']); + }); +}); + +describe('animation renderer factory', () => { + let eventLogs: string[] = []; + let rendererFactory: RendererFactory2|null = null; + + function getAnimationLog(): MockAnimationPlayer[] { + return MockAnimationDriver.log as MockAnimationPlayer[]; + } + + beforeEach(() => { + eventLogs = []; + rendererFactory = null; + MockAnimationDriver.log = []; + + TestBed.configureTestingModule({ + declarations: [SomeComponentWithAnimation, SomeComponent], + providers: [{ + provide: RendererFactory2, + useFactory: (d: any) => rendererFactory = getAnimationRendererFactory2(d), + deps: [DOCUMENT] + }] + }); + }); + + @Component({ + selector: 'some-component', + template: ` +
    + foo +
    + `, + animations: [{ + type: 7, + name: 'myAnimation', + definitions: [{ + type: 1, + expr: '* => on', + animation: [{type: 4, styles: {type: 6, styles: {opacity: 1}, offset: null}, timings: 10}], + options: null + }], + options: {} + }] + }) + class SomeComponentWithAnimation { + exp: string|undefined; + + callback(event: AnimationEvent) { + eventLogs.push(`${event.fromState ? event.fromState : event.toState} - ${event.phaseName}`); + } + } + + @Component({selector: 'some-component', template: 'foo'}) + class SomeComponent { + } + + it('should work with components without animations', () => { + const fixture = TestBed.createComponent(SomeComponent); + fixture.detectChanges(); + expect(fixture.nativeElement.innerHTML).toEqual('foo'); + }); + + isBrowser && it('should work with animated components', (done) => { + const fixture = TestBed.createComponent(SomeComponentWithAnimation); + fixture.detectChanges(); + + expect(rendererFactory).toBeTruthy(); + expect(fixture.nativeElement.innerHTML) + .toMatch(/
    \s+foo\s+<\/div>/); + + fixture.componentInstance.exp = 'on'; + fixture.detectChanges(); + + const [player] = getAnimationLog(); + expect(player.keyframes).toEqual([ + {opacity: '*', offset: 0}, + {opacity: 1, offset: 1}, + ]); + player.finish(); + + rendererFactory !.whenRenderingDone !().then(() => { + expect(eventLogs).toEqual(['void - start', 'void - done', 'on - start', 'on - done']); + done(); + }); + }); +}); + +function getRendererFactory2(document: any): RendererFactory2 { + const fakeNgZone: NgZone = new NoopNgZone(); + const eventManager = new EventManager([], fakeNgZone); + const rendererFactory = new ServerRendererFactory2( + eventManager, fakeNgZone, document, new ɵDomSharedStylesHost(document)); + const origCreateRenderer = rendererFactory.createRenderer; + rendererFactory.createRenderer = function() { + const renderer = origCreateRenderer.apply(this, arguments); + renderer.destroyNode = () => {}; + return renderer; + }; + return rendererFactory; +} + +function getAnimationRendererFactory2(document: any): RendererFactory2 { + const fakeNgZone: NgZone = new NoopNgZone(); + return new ɵAnimationRendererFactory( + getRendererFactory2(document), + new ɵAnimationEngine( + document.body, new MockAnimationDriver(), new ɵNoopAnimationStyleNormalizer()), + fakeNgZone); +} diff --git a/packages/core/test/acceptance/router_integration_spec.ts b/packages/core/test/acceptance/router_integration_spec.ts new file mode 100644 index 0000000000..bfa5666f2e --- /dev/null +++ b/packages/core/test/acceptance/router_integration_spec.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google Inc. 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 {APP_BASE_HREF} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {Router, RouterModule} from '@angular/router'; + +describe('router integration acceptance', () => { + // Test case that ensures that we don't regress in multi-provider ordering + // which is leveraged in the router. See: FW-1349 + it('should have correct order for multiple routes declared in different modules', () => { + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '1a:1', redirectTo: ''}, + {path: '1a:2', redirectTo: ''}, + ]), + ], + }) + class Level1AModule { + } + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '1b:1', redirectTo: ''}, + {path: '1b:2', redirectTo: ''}, + ]), + ], + }) + class Level1BModule { + } + + @NgModule({ + imports: [ + RouterModule.forRoot([{path: 'root', redirectTo: ''}]), + Level1AModule, + Level1BModule, + ], + providers: [ + {provide: APP_BASE_HREF, useValue: '/'}, + ] + }) + class RootModule { + } + + TestBed.configureTestingModule({ + imports: [RootModule], + }); + expect((TestBed.get(Router) as Router).config.map(r => r.path)).toEqual([ + '1a:1', + '1a:2', + '1b:1', + '1b:2', + 'root', + ]); + }); +}); diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts new file mode 100644 index 0000000000..4c5b50cd1b --- /dev/null +++ b/packages/core/test/acceptance/styling_next_spec.ts @@ -0,0 +1,767 @@ +/** + * @license + * Copyright Google Inc. 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 {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state'; +import {Component, Directive, HostBinding, Input, ViewChild} from '@angular/core'; +import {SecurityContext} from '@angular/core/src/core'; +import {getLContext} from '@angular/core/src/render3/context_discovery'; +import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/instructions/lview_debug'; +import {SANITIZER} from '@angular/core/src/render3/interfaces/view'; +import {RuntimeStylingMode, runtimeSetStylingMode, setCurrentStyleSanitizer} from '@angular/core/src/render3/styling_next/state'; +import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils'; +import {ngDevModeResetPerfCounters as resetStylingCounters} from '@angular/core/src/util/ng_dev_mode'; +import {TestBed} from '@angular/core/testing'; +import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; + +describe('new styling integration', () => { + beforeEach(() => { + runtimeSetStylingMode(RuntimeStylingMode.UseNew); + compilerSetStylingMode(CompilerStylingMode.UseNew); + }); + + afterEach(() => { + runtimeSetStylingMode(RuntimeStylingMode.UseOld); + compilerSetStylingMode(CompilerStylingMode.UseOld); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should apply single property styles/classes to the element and default to any static styling values', + () => { + @Component({ + template: ` +
    + ` + }) + class Cmp { + w: string|null = '100px'; + h: string|null = '100px'; + o: string|null = '0.5'; + abc = true; + xyz = false; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + expect(element.style.height).toEqual('100px'); + expect(element.style.opacity).toEqual('0.5'); + expect(element.classList.contains('abc')).toBeTruthy(); + expect(element.classList.contains('xyz')).toBeFalsy(); + + fixture.componentInstance.w = null; + fixture.componentInstance.h = null; + fixture.componentInstance.o = null; + fixture.componentInstance.abc = false; + fixture.componentInstance.xyz = true; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + expect(element.style.height).toEqual('200px'); + expect(element.style.opacity).toBeFalsy(); + expect(element.classList.contains('abc')).toBeFalsy(); + expect(element.classList.contains('xyz')).toBeTruthy(); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should apply single style/class across the template and directive host bindings', () => { + @Directive({selector: '[dir-that-sets-width]'}) + class DirThatSetsWidthDirective { + @Input('dir-that-sets-width') @HostBinding('style.width') public width: string = ''; + } + + @Directive({selector: '[another-dir-that-sets-width]', host: {'[style.width]': 'width'}}) + class AnotherDirThatSetsWidthDirective { + @Input('another-dir-that-sets-width') public width: string = ''; + } + + @Component({ + template: ` +
    + ` + }) + class Cmp { + w0: string|null = null; + w1: string|null = null; + w2: string|null = null; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, DirThatSetsWidthDirective, AnotherDirThatSetsWidthDirective]}); + const fixture = TestBed.createComponent(Cmp); + fixture.componentInstance.w0 = '100px'; + fixture.componentInstance.w1 = '200px'; + fixture.componentInstance.w2 = '300px'; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + + fixture.componentInstance.w0 = null; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + + fixture.componentInstance.w1 = null; + fixture.detectChanges(); + + expect(element.style.width).toEqual('300px'); + + fixture.componentInstance.w2 = null; + fixture.detectChanges(); + + expect(element.style.width).toBeFalsy(); + + fixture.componentInstance.w2 = '400px'; + fixture.detectChanges(); + + expect(element.style.width).toEqual('400px'); + + fixture.componentInstance.w1 = '500px'; + fixture.componentInstance.w0 = '600px'; + fixture.detectChanges(); + + expect(element.style.width).toEqual('600px'); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should combine all styling across the template, directive and component host bindings', + () => { + @Directive({selector: '[dir-with-styling]'}) + class DirWithStyling { + @HostBinding('style.color') public color = 'red'; + + @HostBinding('style.font-size') public fontSize = '100px'; + + @HostBinding('class.dir') public dirClass = true; + } + + @Component({selector: 'comp-with-styling'}) + class CompWithStyling { + @HostBinding('style.width') public width = '900px'; + + @HostBinding('style.height') public height = '900px'; + + @HostBinding('class.comp') public compClass = true; + } + + @Component({ + template: ` + ... + ` + }) + class Cmp { + opacity: string|null = '0.5'; + width: string|null = 'auto'; + tplClass = true; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('comp-with-styling'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + const classes = node.classes !; + + expect(styles.values).toEqual({ + 'color': 'red', + 'width': 'auto', + 'opacity': '0.5', + 'height': '900px', + 'font-size': '100px' + }); + expect(classes.values).toEqual({ + 'dir': true, + 'comp': true, + 'tpl': true, + }); + + fixture.componentInstance.width = null; + fixture.componentInstance.opacity = null; + fixture.componentInstance.tplClass = false; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'color': 'red', + 'width': '900px', + 'opacity': null, + 'height': '900px', + 'font-size': '100px' + }); + expect(classes.values).toEqual({ + 'dir': true, + 'comp': true, + 'tpl': false, + }); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should properly apply styling across sub and super class directive host bindings', + () => { + @Directive({selector: '[super-class-dir]'}) + class SuperClassDirective { + @HostBinding('style.width') public w1 = '100px'; + } + + @Component({selector: '[sub-class-dir]'}) + class SubClassDirective extends SuperClassDirective { + @HostBinding('style.width') public w2 = '200px'; + } + + @Component({ + template: ` +
    + ` + }) + class Cmp { + w3: string|null = '300px'; + } + + TestBed.configureTestingModule( + {declarations: [Cmp, SuperClassDirective, SubClassDirective]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + + expect(styles.values).toEqual({ + 'width': '300px', + }); + + fixture.componentInstance.w3 = null; + fixture.detectChanges(); + expect(styles.values).toEqual({ + 'width': '200px', + }); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should support situations where there are more than 32 bindings', () => { + const TOTAL_BINDINGS = 34; + + let bindingsHTML = ''; + let bindingsArr: any[] = []; + for (let i = 0; i < TOTAL_BINDINGS; i++) { + bindingsHTML += `[style.prop${i}]="bindings[${i}]" `; + bindingsArr.push(null); + } + + @Component({template: `
    `}) + class Cmp { + bindings = bindingsArr; + + updateBindings(value: string) { + for (let i = 0; i < TOTAL_BINDINGS; i++) { + this.bindings[i] = value + i; + } + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + + let testValue = 'initial'; + fixture.componentInstance.updateBindings('initial'); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + + const node = getDebugNode(element) !; + const styles = node.styles !; + + let values = styles.values; + let props = Object.keys(values); + expect(props.length).toEqual(TOTAL_BINDINGS); + + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const value = values[prop] as string; + const num = value.substr(testValue.length); + expect(value).toEqual(`initial${num}`); + } + + testValue = 'final'; + fixture.componentInstance.updateBindings('final'); + fixture.detectChanges(); + + values = styles.values; + props = Object.keys(values); + expect(props.length).toEqual(TOTAL_BINDINGS); + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + const value = values[prop] as string; + const num = value.substr(testValue.length); + expect(value).toEqual(`final${num}`); + } + }); + + onlyInIvy('only ivy has style debugging support') + .it('should apply map-based style and class entries', () => { + @Component({template: '
    '}) + class Cmp { + public c !: {[key: string]: any}; + updateClasses(prop: string) { + this.c = {...this.c || {}}; + this.c[prop] = true; + } + + public s !: {[key: string]: any}; + updateStyles(prop: string, value: string|number|null) { + this.s = {...this.s || {}}; + this.s[prop] = value; + } + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + comp.updateStyles('width', '100px'); + comp.updateStyles('height', '200px'); + comp.updateClasses('abc'); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + const classes = node.classes !; + + const stylesSummary = styles.summary; + const widthSummary = stylesSummary['width']; + expect(widthSummary.prop).toEqual('width'); + expect(widthSummary.value).toEqual('100px'); + + const heightSummary = stylesSummary['height']; + expect(heightSummary.prop).toEqual('height'); + expect(heightSummary.value).toEqual('200px'); + + const classesSummary = classes.summary; + const abcSummary = classesSummary['abc']; + expect(abcSummary.prop).toEqual('abc'); + expect(abcSummary.value as any).toEqual(true); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should resolve styling collisions across templates, directives and components for prop and map-based entries', + () => { + @Directive({selector: '[dir-that-sets-styling]'}) + class DirThatSetsStyling { + @HostBinding('style') public map: any = {color: 'red', width: '777px'}; + } + + @Component({ + template: ` +
    + ` + }) + class Cmp { + map: any = {width: '111px', opacity: '0.5'}; + width: string|null = '555px'; + + @ViewChild('dir', {read: DirThatSetsStyling, static: true}) + dir !: DirThatSetsStyling; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + + const styles = node.styles !; + expect(styles.values).toEqual({ + 'width': '555px', + 'color': 'red', + 'font-size': '99px', + 'opacity': '0.5', + }); + + comp.width = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '111px', + 'color': 'red', + 'font-size': '99px', + 'opacity': '0.5', + }); + + comp.map = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '777px', + 'color': 'red', + 'font-size': '99px', + 'opacity': null, + }); + + comp.dir.map = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '200px', + 'color': null, + 'font-size': '99px', + 'opacity': null, + }); + }); + + onlyInIvy('ivy resolves styling across directives, components and templates in unison') + .it('should only apply each styling property once per CD across templates, components, directives', + () => { + @Directive({selector: '[dir-that-sets-styling]'}) + class DirThatSetsStyling { + @HostBinding('style') public map: any = {width: '999px', height: '999px'}; + } + + @Component({ + template: ` +
    + ` + }) + class Cmp { + width: string|null = '111px'; + height: string|null = '111px'; + + map: any = {width: '555px', height: '555px'}; + + @ViewChild('dir', {read: DirThatSetsStyling, static: true}) + dir !: DirThatSetsStyling; + } + + TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + + resetStylingCounters(); + fixture.detectChanges(); + const element = fixture.nativeElement.querySelector('div'); + + // both are applied because this is the first pass + assertStyleCounters(2, 0); + assertStyle(element, 'width', '111px'); + assertStyle(element, 'height', '111px'); + + comp.width = '222px'; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '222px'); + assertStyle(element, 'height', '111px'); + + comp.height = '222px'; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '222px'); + assertStyle(element, 'height', '222px'); + + comp.width = null; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '555px'); + assertStyle(element, 'height', '222px'); + + comp.width = '123px'; + comp.height = '123px'; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyle(element, 'width', '123px'); + assertStyle(element, 'height', '123px'); + + comp.map = {}; + resetStylingCounters(); + fixture.detectChanges(); + + // both are applied because the map was altered + assertStyleCounters(2, 0); + assertStyle(element, 'width', '123px'); + assertStyle(element, 'height', '123px'); + + comp.width = null; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '999px'); + assertStyle(element, 'height', '123px'); + + comp.dir.map = null; + resetStylingCounters(); + fixture.detectChanges(); + + // both are applied because the map was altered + assertStyleCounters(2, 0); + assertStyle(element, 'width', '0px'); + assertStyle(element, 'height', '123px'); + + comp.dir.map = {width: '1000px', height: '1000px', color: 'red'}; + resetStylingCounters(); + fixture.detectChanges(); + + // all three are applied because the map was altered + assertStyleCounters(3, 0); + assertStyle(element, 'width', '1000px'); + assertStyle(element, 'height', '123px'); + assertStyle(element, 'color', 'red'); + + comp.height = null; + resetStylingCounters(); + fixture.detectChanges(); + + assertStyleCounters(1, 0); + assertStyle(element, 'width', '1000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'red'); + + comp.map = {color: 'blue', width: '2000px', opacity: '0.5'}; + resetStylingCounters(); + fixture.detectChanges(); + + // all four are applied because the map was altered + assertStyleCounters(4, 0); + assertStyle(element, 'width', '2000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'blue'); + assertStyle(element, 'opacity', '0.5'); + + comp.map = {color: 'blue', width: '2000px'}; + resetStylingCounters(); + fixture.detectChanges(); + + // all four are applied because the map was altered + assertStyleCounters(3, 1); + assertStyle(element, 'width', '2000px'); + assertStyle(element, 'height', '1000px'); + assertStyle(element, 'color', 'blue'); + assertStyle(element, 'opacity', ''); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should sanitize style values before writing them', () => { + @Component({ + template: ` +
    + ` + }) + class Cmp { + widthExp = ''; + bgImageExp = ''; + styleMapExp: any = {}; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + const lastSanitizedProps: any[] = []; + styles.overrideSanitizer((prop, value) => { + lastSanitizedProps.push(prop); + return value; + }); + + comp.bgImageExp = '123'; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'width': null, + }); + + expect(lastSanitizedProps).toEqual(['background-image']); + lastSanitizedProps.length = 0; + + comp.styleMapExp = {'clip-path': '456'}; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'clip-path': '456', + 'width': null, + }); + + expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); + lastSanitizedProps.length = 0; + + comp.widthExp = '789px'; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'background-image': '123', + 'clip-path': '456', + 'width': '789px', + }); + + expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); + lastSanitizedProps.length = 0; + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should apply a unit to a style before writing it', () => { + @Component({ + template: ` +
    + ` + }) + class Cmp { + widthExp: string|number|null = ''; + heightExp: string|number|null = ''; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const node = getDebugNode(element) !; + const styles = node.styles !; + + comp.widthExp = '200'; + comp.heightExp = 10; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '200px', + 'height': '10em', + }); + + comp.widthExp = 0; + comp.heightExp = null; + fixture.detectChanges(); + + expect(styles.values).toEqual({ + 'width': '0px', + 'height': null, + }); + }); + + onlyInIvy('only ivy has style/class bindings debugging support') + .it('should pick up and use the sanitizer present in the lView', () => { + @Component({ + template: ` +
    + ` + }) + class Cmp { + w = '100px'; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('div'); + const lView = getLContext(element) !.lView; + lView[SANITIZER] = new MockSanitizer(value => { return `${value}-safe`; }); + + comp.w = '200px'; + fixture.detectChanges(); + + const node = getDebugNode(element) !; + const styles = node.styles !; + expect(styles.values['width']).toEqual('200px-safe'); + + // this is here so that it won't get picked up accidentally in another test + lView[SANITIZER] = null; + setCurrentStyleSanitizer(null); + }); + + it('should be able to bind a SafeValue to clip-path', () => { + @Component({template: '
    '}) + class Cmp { + path !: SafeStyle; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const sanitizer: DomSanitizer = TestBed.get(DomSanitizer); + + fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")'); + fixture.detectChanges(); + + const html = fixture.nativeElement.innerHTML; + + // Note that check the raw HTML, because (at the time of writing) the Node-based renderer + // that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`. + expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/); + }); +}); + +function assertStyleCounters(countForSet: number, countForRemove: number) { + expect(ngDevMode !.rendererSetStyle).toEqual(countForSet); + expect(ngDevMode !.rendererRemoveStyle).toEqual(countForRemove); +} + +function assertStyle(element: HTMLElement, prop: string, value: any) { + expect((element.style as any)[prop]).toEqual(value); +} + +function getDebugNode(element: Node): DebugNode|null { + const lContext = loadLContextFromNode(element); + const lViewDebug = toDebug(lContext.lView) as LViewDebug; + const debugNodes = lViewDebug.nodes || []; + for (let i = 0; i < debugNodes.length; i++) { + const n = debugNodes[i]; + if (n.native === element) { + return n; + } + } + return null; +} + +class MockSanitizer { + constructor(private _interceptorFn: ((value: any) => any)) {} + sanitize(context: SecurityContext, value: any): string|null { return this._interceptorFn(value); } +} diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index fa5e7237b6..4b66286004 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -160,4 +160,44 @@ describe('styling', () => { tNode: 3, }); }); + + it('should not throw if host style binding is on a template node', () => { + // This ex is a bit contrived. In real apps, you might have a shared class that is extended both + // by components with host elements and by directives on template nodes. In that case, the host + // styles for the template directives should just be ignored. + @Directive({selector: 'ng-template[styleDir]', host: {'[style.display]': 'display'}}) + class StyleDir { + display = 'block'; + } + + @Component({selector: 'app-comp', template: ``}) + class MyApp { + } + + TestBed.configureTestingModule({declarations: [MyApp, StyleDir]}); + expect(() => { + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + }).not.toThrow(); + }); + + it('should be able to bind a SafeValue to clip-path', () => { + @Component({template: '
    '}) + class Cmp { + path !: SafeStyle; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const sanitizer: DomSanitizer = TestBed.get(DomSanitizer); + + fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")'); + fixture.detectChanges(); + + const html = fixture.nativeElement.innerHTML; + + // Note that check the raw HTML, because (at the time of writing) the Node-based renderer + // that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`. + expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/); + }); }); diff --git a/packages/core/test/acceptance/template_ref_spec.ts b/packages/core/test/acceptance/template_ref_spec.ts index 981cc43bac..447f893876 100644 --- a/packages/core/test/acceptance/template_ref_spec.ts +++ b/packages/core/test/acceptance/template_ref_spec.ts @@ -9,6 +9,7 @@ import {Component, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {onlyInIvy} from '@angular/private/testing'; describe('TemplateRef', () => { describe('rootNodes', () => { @@ -24,7 +25,7 @@ describe('TemplateRef', () => { exportAs: 'menuContent' }) class MenuContent { - @ViewChild(TemplateRef) template !: TemplateRef; + @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; } @Component({ @@ -36,7 +37,7 @@ describe('TemplateRef', () => { ` }) class App { - @ViewChild(MenuContent) content !: MenuContent; + @ViewChild(MenuContent, {static: false}) content !: MenuContent; constructor(public viewContainerRef: ViewContainerRef) {} } @@ -52,6 +53,117 @@ describe('TemplateRef', () => { expect(rootNodeTextContent).toEqual(['Header', 'Item one', 'Item two']); }); - }); + it('should return root render nodes for an embedded view instance', () => { + @Component({ + template: ` + +
    + some text + +
    + ` + }) + class App { + @ViewChild('templateRef', {static: true}) + templateRef !: TemplateRef; + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({}); + expect(embeddedView.rootNodes.length).toBe(3); + }); + + /** + * This is different as compared to the view engine implementation which returns a comment node + * in this case: + * https://stackblitz.com/edit/angular-uiqry6?file=src/app/app.component.ts + * + * Returning a comment node for a template ref with no nodes is wrong is fixed in Ivy. + */ + onlyInIvy('Fixed: Ivy no longer adds a comment node in this case.') + .it('should return an empty array for embedded view with no nodes', () => { + @Component({ + template: ` + + ` + }) + class App { + @ViewChild('templateRef', {static: true}) + templateRef !: TemplateRef; + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({}); + expect(embeddedView.rootNodes.length).toBe(0); + }); + + it('should not descend into containers when retrieving root nodes', () => { + /** + * NOTE: In VE, if `SUFFIX` text node below is _not_ present, VE will add an + * additional `` comment, thus being slightly different than Ivy. + * (resulting in 1 root node in Ivy and 2 in VE). + */ + @Component({ + template: ` + textSUFFIX + ` + }) + class App { + @ViewChild('templateRef', {static: true}) + templateRef !: TemplateRef; + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({}); + expect(embeddedView.rootNodes.length).toBe(2); + expect(embeddedView.rootNodes[0].nodeType).toBe(Node.COMMENT_NODE); + expect(embeddedView.rootNodes[1].nodeType).toBe(Node.TEXT_NODE); + }); + + /** + * Contrary to containers () we _do_ descend into element containers + * () + */ + it('should descend into element containers when retrieving root nodes', () => { + @Component({ + template: ` + + text + + ` + }) + class App { + @ViewChild('templateRef', {static: true}) + templateRef !: TemplateRef; + } + + TestBed.configureTestingModule({ + declarations: [App], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const embeddedView = fixture.componentInstance.templateRef.createEmbeddedView({}); + + expect(embeddedView.rootNodes.length).toBe(2); + expect(embeddedView.rootNodes[0].nodeType).toBe(Node.COMMENT_NODE); + expect(embeddedView.rootNodes[1].nodeType).toBe(Node.TEXT_NODE); + }); + }); }); diff --git a/packages/core/test/acceptance/text_spec.ts b/packages/core/test/acceptance/text_spec.ts new file mode 100644 index 0000000000..3f3e665f06 --- /dev/null +++ b/packages/core/test/acceptance/text_spec.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright Google Inc. 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 {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {of } from 'rxjs'; + +describe('text instructions', () => { + it('should handle all flavors of interpolated text', () => { + @Component({ + template: ` +
    a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h{{eight}}i{{nine}}j
    +
    a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h{{eight}}i
    +
    a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g{{seven}}h
    +
    a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f{{six}}g
    +
    a{{one}}b{{two}}c{{three}}d{{four}}e{{five}}f
    +
    a{{one}}b{{two}}c{{three}}d{{four}}e
    +
    a{{one}}b{{two}}c{{three}}d
    +
    a{{one}}b{{two}}c
    +
    a{{one}}b
    +
    {{one}}
    + ` + }) + class App { + one = 1; + two = 2; + three = 3; + four = 4; + five = 5; + six = 6; + seven = 7; + eight = 8; + nine = 9; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const allTextContent = Array.from(fixture.nativeElement.querySelectorAll('div')) + .map((div: HTMLDivElement) => div.textContent); + + expect(allTextContent).toEqual([ + 'a1b2c3d4e5f6g7h8i9j', + 'a1b2c3d4e5f6g7h8i', + 'a1b2c3d4e5f6g7h', + 'a1b2c3d4e5f6g', + 'a1b2c3d4e5f', + 'a1b2c3d4e', + 'a1b2c3d', + 'a1b2c', + 'a1b', + '1', + ]); + }); + + it('should handle piped values in interpolated text', () => { + @Component({ + template: ` +

    {{who | async}} sells {{(item | async)?.what}} down by the {{(item | async)?.where}}.

    + ` + }) + class App { + who = of ('Sally'); + item = of ({ + what: 'seashells', + where: 'seashore', + }); + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const p = fixture.nativeElement.querySelector('p') as HTMLDivElement; + expect(p.textContent).toBe('Sally sells seashells down by the seashore.'); + }); + + it('should not sanitize urls in interpolated text', () => { + @Component({ + template: '

    {{thisisfine}}

    ', + }) + class App { + thisisfine = 'javascript:alert("image_of_dog_with_coffee_in_burning_building.gif")'; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const p = fixture.nativeElement.querySelector('p'); + + expect(p.textContent) + .toBe('javascript:alert("image_of_dog_with_coffee_in_burning_building.gif")'); + }); + + it('should not allow writing HTML in interpolated text', () => { + @Component({ + template: '
    {{test}}
    ', + }) + class App { + test = '

    LOL, big text

    '; + } + + TestBed.configureTestingModule({declarations: [App]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + const div = fixture.nativeElement.querySelector('div'); + + expect(div.innerHTML).toBe('<h1>LOL, big text</h1>'); + }); +}); diff --git a/packages/core/test/acceptance/view_container_ref_spec.ts b/packages/core/test/acceptance/view_container_ref_spec.ts index 82d08ed6e2..97eb4f8db0 100644 --- a/packages/core/test/acceptance/view_container_ref_spec.ts +++ b/packages/core/test/acceptance/view_container_ref_spec.ts @@ -6,8 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ElementRef, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; +import {CommonModule, DOCUMENT} from '@angular/common'; +import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, NO_ERRORS_SCHEMA, NgModule, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; +import {Input} from '@angular/core/src/metadata'; +import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; +import {TestBed, TestComponentRenderer} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; @@ -21,6 +25,15 @@ describe('ViewContainerRef', () => { '{$startTagDiv}{$closeTagDiv}{$startTagBefore}{$closeTagBefore}' }; + /** + * Gets the inner HTML of the given element with all HTML comments and Angular internal + * reflect attributes omitted. This makes HTML comparisons easier and less verbose. + */ + function getElementHtml(element: Element) { + return element.innerHTML.replace(//g, '') + .replace(/\sng-reflect-\S*="[^"]*"/g, ''); + } + beforeEach(() => { ɵi18nConfigureLocalize({translations: TRANSLATIONS}); TestBed.configureTestingModule({ @@ -48,6 +61,59 @@ describe('ViewContainerRef', () => { expect(fixture.componentInstance.foo).toBeAnInstanceOf(TemplateRef); }); + it('should use comment node of host ng-container as insertion marker', () => { + @Component({template: 'hello'}) + class HelloComp { + } + + @NgModule({entryComponents: [HelloComp], declarations: [HelloComp]}) + class HelloCompModule { + } + + @Component({ + template: ` + + ` + }) + class TestComp { + @ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective; + } + + TestBed.configureTestingModule( + {declarations: [TestComp, VCRefDirective], imports: [HelloCompModule]}); + const fixture = TestBed.createComponent(TestComp); + const {vcref, cfr, elementRef} = fixture.componentInstance.vcRefDir; + fixture.detectChanges(); + + expect(fixture.nativeElement.innerHTML) + .toMatch(//, 'Expected only one comment node to be generated.'); + + const testParent = document.createElement('div'); + testParent.appendChild(elementRef.nativeElement); + + expect(testParent.textContent).toBe(''); + expect(testParent.childNodes.length).toBe(1); + expect(testParent.childNodes[0].nodeType).toBe(Node.COMMENT_NODE); + + // Add a test component to the view container ref to ensure that + // the "ng-container" comment was used as marker for the insertion. + vcref.createComponent(cfr.resolveComponentFactory(HelloComp)); + fixture.detectChanges(); + + expect(testParent.textContent).toBe('hello'); + expect(testParent.childNodes.length).toBe(2); + + // With Ivy, views are inserted before the container comment marker. + if (ivyEnabled) { + expect(testParent.childNodes[0].nodeType).toBe(Node.ELEMENT_NODE); + expect(testParent.childNodes[0].textContent).toBe('hello'); + expect(testParent.childNodes[1].nodeType).toBe(Node.COMMENT_NODE); + } else { + expect(testParent.childNodes[0].nodeType).toBe(Node.COMMENT_NODE); + expect(testParent.childNodes[1].nodeType).toBe(Node.ELEMENT_NODE); + expect(testParent.childNodes[1].textContent).toBe('hello'); + } + }); }); describe('insert', () => { @@ -269,8 +335,1404 @@ describe('ViewContainerRef', () => { }); }); + describe('length', () => { + it('should return the number of embedded views', () => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(vcRefDir.vcref.length).toEqual(0); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(3); + + vcRefDir.vcref.detach(1); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(2); + + vcRefDir.vcref.clear(); + fixture.detectChanges(); + expect(vcRefDir.vcref.length).toEqual(0); + }); + }); + + describe('get and indexOf', () => { + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + }); + + it('should retrieve a ViewRef from its index, and vice versa', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + fixture.detectChanges(); + + let viewRef = vcRefDir.vcref.get(0); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(0); + + viewRef = vcRefDir.vcref.get(1); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(1); + + viewRef = vcRefDir.vcref.get(2); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(2); + }); + + it('should handle out of bounds cases', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + fixture.detectChanges(); + + expect(vcRefDir.vcref.get(-1)).toBeNull(); + expect(vcRefDir.vcref.get(42)).toBeNull(); + + const viewRef = vcRefDir.vcref.get(0); + vcRefDir.vcref.remove(0); + expect(vcRefDir.vcref.indexOf(viewRef !)).toEqual(-1); + }); + }); + + describe('move', () => { + it('should move embedded views and associated DOM nodes without recreating them', () => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABC'); + + // The DOM is manually modified here to ensure that the text node is actually moved + fixture.nativeElement.childNodes[2].nodeValue = '**A**'; + expect(getElementHtml(fixture.nativeElement)).toEqual('

    **A**BC'); + + let viewRef = vcRefDir.vcref.get(0); + vcRefDir.vcref.move(viewRef !, 2); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    BC**A**'); + + vcRefDir.vcref.move(viewRef !, 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    **A**BC'); + + vcRefDir.vcref.move(viewRef !, 1); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    B**A**C'); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.move(viewRef !, -1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.move(viewRef !, 42)).toThrow(); + }); + }); + + describe('getters', () => { + it('should work on templates', () => { + @Component({ + template: ` + {{name}} +
    + ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [VCRefDirective, TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + const {vcRefDir} = fixture.componentInstance; + fixture.detectChanges(); + + expect(vcRefDir.vcref.element.nativeElement.nodeType).toBe(Node.COMMENT_NODE); + // In Ivy, the comment for the view container ref has text that implies + // that the comment is a placeholder for a container. + ivyEnabled && expect(vcRefDir.vcref.element.nativeElement.textContent).toEqual('container'); + + expect(vcRefDir.vcref.injector.get(ElementRef).nativeElement.textContent); + expect(getElementHtml(vcRefDir.vcref.parentInjector.get(ElementRef).nativeElement)) + .toBe('
    '); + }); + }); + + describe('detach', () => { + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + + // Tests depend on perf counters when running with Ivy. In order to have + // clean perf counters at the beginning of a test, we reset those here. + ivyEnabled && ngDevModeResetPerfCounters(); + }); + + it('should detach the right embedded view when an index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + const viewD = vcRefDir.createView('D'); + vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCDE'); + + vcRefDir.vcref.detach(3); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCE'); + expect(viewD.destroyed).toBeFalsy(); + + vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    BCE'); + expect(viewA.destroyed).toBeFalsy(); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.detach(-1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.detach(42)).toThrow(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(0); + }); + + it('should detach the last embedded view when no index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + vcRefDir.createView('D'); + const viewE = vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCDE'); + + vcRefDir.vcref.detach(); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCD'); + expect(viewE.destroyed).toBeFalsy(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(0); + }); + }); + + describe('remove', () => { + beforeEach(() => { + TestBed.configureTestingModule({declarations: [EmbeddedViewInsertionComp, VCRefDirective]}); + + const _origRendererFactory = TestBed.get(RendererFactory2) as RendererFactory2; + const _origCreateRenderer = _origRendererFactory.createRenderer; + + _origRendererFactory.createRenderer = function() { + const renderer = _origCreateRenderer.apply(_origRendererFactory, arguments); + renderer.destroyNode = () => {}; + return renderer; + }; + + // Tests depend on perf counters when running with Ivy. In order to have + // clean perf counters at the beginning of a test, we reset those here. + ivyEnabled && ngDevModeResetPerfCounters(); + }); + + it('should remove the right embedded view when an index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + const viewD = vcRefDir.createView('D'); + vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCDE'); + + vcRefDir.vcref.remove(3); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCE'); + expect(viewD.destroyed).toBeTruthy(); + + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    BCE'); + expect(viewA.destroyed).toBeTruthy(); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRefDir.vcref.remove(-1)).toThrow(); + ivyEnabled && expect(() => vcRefDir.vcref.remove(42)).toThrow(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(2); + }); + + it('should remove the last embedded view when no index is specified', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.createView('A'); + vcRefDir.createView('B'); + vcRefDir.createView('C'); + vcRefDir.createView('D'); + const viewE = vcRefDir.createView('E'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCDE'); + + vcRefDir.vcref.remove(); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    ABCD'); + expect(viewE.destroyed).toBeTruthy(); + ivyEnabled && expect(ngDevMode !.rendererDestroyNode).toBe(1); + }); + + it('should throw when trying to insert a removed or destroyed view', () => { + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + const viewA = vcRefDir.createView('A'); + const viewB = vcRefDir.createView('B'); + fixture.detectChanges(); + + vcRefDir.vcref.remove(); + fixture.detectChanges(); + expect(() => vcRefDir.vcref.insert(viewB)).toThrow(); + + viewA.destroy(); + fixture.detectChanges(); + expect(() => vcRefDir.vcref.insert(viewA)).toThrow(); + }); + }); + + describe('createEmbeddedView (incl. insert)', () => { + it('should work on elements', () => { + @Component({ + template: ` + {{name}} +
    +
    + `, + }) + class TestComponent { + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + + const fixture = TestBed.createComponent(TestComponent); + const vcRef = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    '); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    A
    '); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    ABC
    '); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    YABC
    '); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef.createView('Z', 5)).toThrow(); + }); + + it('should work on components', () => { + @Component({selector: 'header-cmp', template: ``}) + class HeaderComponent { + } + + @Component({ + template: ` + {{name}} + +
    + `, + }) + class TestComponent { + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, HeaderComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + const vcRef = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    '); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('A
    '); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ABC
    '); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('YABC
    '); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef.createView('Z', 5)).toThrow(); + }); + + it('should work with multiple instances of view container refs', () => { + @Component({ + template: ` + {{name}} +
    +
    + `, + }) + class TestComponent { + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + const vcRefs = fixture.debugElement.queryAll(By.directive(VCRefDirective)) + .map(debugEl => debugEl.injector.get(VCRefDirective)); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    '); + + vcRefs[0].createView('A'); + vcRefs[1].createView('B'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    A
    B'); + }); + + it('should work on templates', () => { + @Component({ + template: ` + {{name}} +
    + ` + }) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRef !: VCRefDirective; + } + + TestBed.configureTestingModule({declarations: [TestComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const {vcRef} = fixture.componentInstance; + + expect(getElementHtml(fixture.nativeElement)).toEqual('
    '); + + vcRef.createView('A'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('A
    '); + + vcRef.createView('B'); + vcRef.createView('C'); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('ABC
    '); + + vcRef.createView('Y', 0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('YABC
    '); + + // Invalid indices when detaching throws an exception in Ivy: FW-1330. + ivyEnabled && expect(() => vcRef !.createView('Z', -1)).toThrow(); + ivyEnabled && expect(() => vcRef !.createView('Z', 5)).toThrow(); + }); + + it('should apply directives and pipes of the host view to the TemplateRef', () => { + @Component({selector: 'child', template: `{{name}}`}) + class Child { + @Input() name: string|undefined; + } + + @Pipe({name: 'starPipe'}) + class StarPipe implements PipeTransform { + transform(value: any) { return `**${value}**`; } + } + + @Component({ + template: ` + + + + + + ` + }) + class SomeComponent { + } + + TestBed.configureTestingModule( + {declarations: [Child, StarPipe, SomeComponent, VCRefDirective]}); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '**A****C****C****B**'); + }); + + }); + + describe('createComponent', () => { + let templateExecutionCounter = 0; + + beforeEach(() => templateExecutionCounter = 0); + + it('should work without Injector and NgModuleRef', () => { + @Component({selector: 'embedded-cmp', template: `foo`}) + class EmbeddedComponent implements DoCheck, OnInit { + ngOnInit() { templateExecutionCounter++; } + + ngDoCheck() { templateExecutionCounter++; } + } + + @NgModule({entryComponents: [EmbeddedComponent], declarations: [EmbeddedComponent]}) + class EmbeddedComponentModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + expect(templateExecutionCounter).toEqual(0); + + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent)); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

    foo'); + expect(templateExecutionCounter).toEqual(2); + + vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + expect(templateExecutionCounter).toEqual(2); + + vcRefDir.vcref.insert(componentRef.hostView); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

    foo'); + expect(templateExecutionCounter).toEqual(3); + }); + + it('should work with NgModuleRef and Injector', () => { + @Component({ + selector: 'embedded-cmp', + template: `foo`, + }) + class EmbeddedComponent implements DoCheck, + OnInit { + constructor(public s: String) {} + + ngOnInit() { templateExecutionCounter++; } + + ngDoCheck() { templateExecutionCounter++; } + } + + @NgModule({entryComponents: [EmbeddedComponent], declarations: [EmbeddedComponent]}) + class EmbeddedComponentModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentModule] + }); + + @NgModule({ + providers: [ + {provide: String, useValue: 'root_module'}, + // We need to provide the following tokens because otherwise view engine + // will throw when creating a component factory in debug mode. + {provide: Sanitizer, useValue: TestBed.get(Sanitizer)}, + {provide: ErrorHandler, useValue: TestBed.get(ErrorHandler)}, + {provide: RendererFactory2, useValue: TestBed.get(RendererFactory2)}, + ] + }) + class MyAppModule { + } + + @NgModule({providers: [{provide: String, useValue: 'some_module'}]}) + class SomeModule { + } + + // Compile test modules in order to be able to pass the NgModuleRef or the + // module injector to the ViewContainerRef create component method. + const compiler = TestBed.get(Compiler) as Compiler; + const appModuleFactory = compiler.compileModuleSync(MyAppModule); + const someModuleFactory = compiler.compileModuleSync(SomeModule); + const appModuleRef = appModuleFactory.create(null); + const someModuleRef = someModuleFactory.create(null); + + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + expect(templateExecutionCounter).toEqual(0); + + let componentRef = vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent), 0, someModuleRef.injector); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('

    foo'); + expect(templateExecutionCounter).toEqual(2); + expect(componentRef.instance.s).toEqual('some_module'); + + componentRef = vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined, undefined, + appModuleRef); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    foofoo'); + expect(componentRef.instance.s).toEqual('root_module'); + expect(templateExecutionCounter).toEqual(5); + }); + + it('should support projectable nodes', () => { + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentWithNgZoneModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, undefined, + [[myNode]]); + fixture.detectChanges(); + + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    barbaz

    '); + }); + + it('should support reprojection of projectable nodes', () => { + @Component({ + selector: 'reprojector', + template: + ``, + }) + class Reprojector { + } + + @NgModule({ + exports: [Reprojector, EmbeddedComponentWithNgContent], + declarations: [Reprojector, EmbeddedComponentWithNgContent], + entryComponents: [Reprojector] + }) + class ReprojectorModule { + } + + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [ReprojectorModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + + const myNode = document.createElement('div'); + const myText = document.createTextNode('bar'); + const myText2 = document.createTextNode('baz'); + myNode.appendChild(myText); + myNode.appendChild(myText2); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(Reprojector), 0, undefined, [[myNode]]); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '


    barbaz
    '); + }); + + it('should support many projectable nodes with many slots', () => { + TestBed.configureTestingModule({ + declarations: [EmbeddedViewInsertionComp, VCRefDirective], + imports: [EmbeddedComponentWithNgZoneModule] + }); + const fixture = TestBed.createComponent(EmbeddedViewInsertionComp); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)).toEqual('

    '); + + vcRefDir.vcref.createComponent( + vcRefDir.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, undefined, [ + [document.createTextNode('1'), document.createTextNode('2')], + [document.createTextNode('3'), document.createTextNode('4')] + ]); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    12
    34
    '); + }); + }); + + describe('insertion points and declaration points', () => { + @Directive({selector: '[tplDir]'}) + class InsertionDir { + @Input() + set tplDir(tpl: TemplateRef|null) { + tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear(); + } + + constructor(public vcr: ViewContainerRef) {} + } + + // see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6 + it('should work with a template declared in a different component view from insertion', () => { + @Component({selector: 'child', template: `
    {{name}}
    `}) + class Child { + @Input() tpl: TemplateRef|null = null; + name = 'Child'; + } + + @Component({ + template: ` + +
    {{name}}
    +
    + + + ` + }) + class Parent { + name = 'Parent'; + } + + TestBed.configureTestingModule({declarations: [Child, Parent, InsertionDir]}); + const fixture = TestBed.createComponent(Parent); + const child = fixture.debugElement.query(By.directive(Child)).componentInstance; + fixture.detectChanges(); + + // Context should be inherited from the declaration point, not the + // insertion point, so the template should read 'Parent'. + expect(getElementHtml(fixture.nativeElement)) + .toEqual(`
    Child
    Parent
    `); + + child.tpl = null; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toEqual(`
    Child
    `); + }); + + // see running stackblitz example: https://stackblitz.com/edit/angular-3vplec + it('should work with nested for loops with different declaration / insertion points', () => { + @Component({ + selector: 'loop-comp', + template: ` + + + `, + }) + class LoopComp { + @Input() tpl !: TemplateRef; + @Input() rows !: any[]; + name = 'Loop'; + } + + @Component({ + template: ` + + +
    {{cell}} - {{row.value}} - {{name}}
    +
    + + +
    + + + `, + }) + class Parent { + name = 'Parent'; + rows = [{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}]; + } + + TestBed.configureTestingModule({declarations: [LoopComp, Parent], imports: [CommonModule]}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '' + + '
    1 - one - Parent
    2 - one - Parent
    ' + + '
    3 - two - Parent
    4 - two - Parent
    ' + + '
    '); + + fixture.componentInstance.rows = + [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}]; + fixture.componentInstance.name = 'New name!'; + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '' + + '
    5 - three - New name!
    6 - three - New name!
    ' + + '
    7 - four - New name!
    ' + + '
    '); + }); + + }); + + describe('lifecycle hooks', () => { + + // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref + const log: string[] = []; + + @Component({selector: 'hooks', template: `{{name}}`}) + class ComponentWithHooks { + @Input() name: string|undefined; + + private log(msg: string) { log.push(msg); } + + ngOnChanges() { this.log('onChanges-' + this.name); } + ngOnInit() { this.log('onInit-' + this.name); } + ngDoCheck() { this.log('doCheck-' + this.name); } + + ngAfterContentInit() { this.log('afterContentInit-' + this.name); } + ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } + + ngAfterViewInit() { this.log('afterViewInit-' + this.name); } + ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } + + ngOnDestroy() { this.log('onDestroy-' + this.name); } + } + + @NgModule({ + declarations: [ComponentWithHooks], + exports: [ComponentWithHooks], + entryComponents: [ComponentWithHooks] + }) + class ComponentWithHooksModule { + } + + it('should call all hooks in correct order when creating with createEmbeddedView', () => { + @Component({ + template: ` + + + + + + ` + }) + class SomeComponent { + } + + log.length = 0; + + TestBed.configureTestingModule({ + declarations: [SomeComponent, ComponentWithHooks, VCRefDirective], + }); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('AB'); + expect(log).toEqual([]); + + log.length = 0; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ACB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C', + 'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const viewRef = vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.insert(viewRef !); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'onDestroy-C', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); + + it('should call all hooks in correct order when creating with createComponent', () => { + @Component({ + template: ` + + + ` + }) + class SomeComponent { + } + + log.length = 0; + + TestBed.configureTestingModule( + {declarations: [SomeComponent, VCRefDirective], imports: [ComponentWithHooksModule]}); + const fixture = TestBed.createComponent(SomeComponent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + fixture.detectChanges(); + expect(log).toEqual([ + 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', + 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', + 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', + 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(ComponentWithHooks)); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('AB'); + expect(log).toEqual([]); + + componentRef.instance.name = 'D'; + log.length = 0; + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('ADB'); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', + 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + const viewRef = vcRefDir.vcref.detach(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.insert(viewRef !); + fixture.detectChanges(); + expect(log).toEqual([ + 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', + 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' + ]); + + log.length = 0; + vcRefDir.vcref.remove(0); + fixture.detectChanges(); + expect(log).toEqual([ + 'onDestroy-D', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', + 'afterViewChecked-A', 'afterViewChecked-B' + ]); + }); + }); + + describe('host bindings', () => { + + it('should support host bindings on dynamically created components', () => { + @Component( + {selector: 'host-bindings', host: {'id': 'attribute', '[title]': 'title'}, template: ``}) + class HostBindingCmpt { + title = 'initial'; + } + + @Component({template: ``}) + class TestComponent { + @ViewChild(VCRefDirective, {static: true}) vcRefDir !: VCRefDirective; + } + + @NgModule({declarations: [HostBindingCmpt], entryComponents: [HostBindingCmpt]}) + class TestModule { + } + + TestBed.configureTestingModule( + {declarations: [TestComponent, VCRefDirective], imports: [TestModule]}); + const fixture = TestBed.createComponent(TestComponent); + const {vcRefDir} = fixture.componentInstance; + + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)).toBe(''); + + const componentRef = + vcRefDir.vcref.createComponent(vcRefDir.cfr.resolveComponentFactory(HostBindingCmpt)); + fixture.detectChanges(); + + expect(fixture.nativeElement.children[0].tagName).toBe('HOST-BINDINGS'); + expect(fixture.nativeElement.children[0].getAttribute('id')).toBe('attribute'); + expect(fixture.nativeElement.children[0].getAttribute('title')).toBe('initial'); + + componentRef.instance.title = 'changed'; + fixture.detectChanges(); + + expect(fixture.nativeElement.children[0].tagName).toBe('HOST-BINDINGS'); + expect(fixture.nativeElement.children[0].getAttribute('id')).toBe('attribute'); + expect(fixture.nativeElement.children[0].getAttribute('title')).toBe('changed'); + }); + + }); + + describe('projection', () => { + + it('should project the ViewContainerRef content along its host, in an element', () => { + @Component({selector: 'child', template: '
    '}) + class Child { + } + + @Component({ + selector: 'parent', + template: ` + + {{name}} + + + +
    blah
    +
    ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule({declarations: [Child, Parent, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    blah
    '); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + expect(getElementHtml(fixture.nativeElement)) + .toEqual('
    blah
    bar
    '); + }); + + it('should project the ViewContainerRef content along its host, in a view', () => { + @Component({ + selector: 'child-with-view', + template: `Before (inside)--After (inside)` + }) + class ChildWithView { + show: boolean = true; + } + + @Component({ + selector: 'parent', + template: ` + + {{name}} + + + Before projected +
    blah
    + After projected +
    ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule({declarations: [ChildWithView, Parent, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + fixture.detectChanges(); + const vcRefDir = + fixture.debugElement.query(By.directive(VCRefDirective)).injector.get(VCRefDirective); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + 'Before (inside)- Before projected
    blah
    After projected -After (inside)
    '); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + 'Before (inside)- Before projected
    blah
    bar After projected -After (inside)
    '); + }); + + describe('with select', () => { + + @Component({ + selector: 'child-with-selector', + template: ` +

    +

    `, + }) + class ChildWithSelector { + } + + it('should project the ViewContainerRef content along its host, when the host matches a selector', + () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + + +
    blah
    +
    + ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule( + {declarations: [Parent, ChildWithSelector, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = fixture.debugElement.query(By.directive(VCRefDirective)) + .injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    blah

    '); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    blah
    bar

    '); + }); + + it('should not project the ViewContainerRef content, when the host does not match a selector', + () => { + @Component({ + selector: 'parent', + template: ` + + {{name}} + + +
    blah
    +
    + ` + }) + class Parent { + name: string = 'bar'; + } + + TestBed.configureTestingModule( + {declarations: [Parent, ChildWithSelector, VCRefDirective]}); + const fixture = TestBed.createComponent(Parent); + const vcRefDir = fixture.debugElement.query(By.directive(VCRefDirective)) + .injector.get(VCRefDirective); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    blah

    '); + + vcRefDir.vcref.createEmbeddedView(vcRefDir.tplRef !); + fixture.detectChanges(); + + expect(getElementHtml(fixture.nativeElement)) + .toEqual( + '

    blah
    bar

    '); + }); + }); + + }); + + describe('root view container ref', () => { + let containerEl: HTMLElement|null = null; + + beforeEach(() => containerEl = null); + + /** + * Creates a new test component renderer instance that wraps the root element + * in another element. This allows us to test if elements have been inserted into + * the parent element of the root component. + */ + function createTestComponentRenderer(document: any): TestComponentRenderer { + return { + insertRootElement(rootElementId: string) { + const rootEl = document.createElement('div'); + rootEl.id = rootElementId; + + containerEl = document.createElement('div'); + document.body.appendChild(containerEl); + containerEl !.appendChild(rootEl); + } + }; + } + + const TEST_COMPONENT_RENDERER = { + provide: TestComponentRenderer, + useFactory: createTestComponentRenderer, + deps: [DOCUMENT] + }; + + it('should check bindings for components dynamically created by root component', () => { + @Component({ + selector: 'dynamic-cmpt-with-bindings', + template: `check count: {{checkCount}}`, + }) + class DynamicCompWithBindings implements DoCheck { + checkCount = 0; + + ngDoCheck() { this.checkCount++; } + } + + @Component({template: ``}) + class TestComp { + constructor(public vcRef: ViewContainerRef, public cfResolver: ComponentFactoryResolver) {} + } + + @NgModule( + {entryComponents: [DynamicCompWithBindings], declarations: [DynamicCompWithBindings]}) + class DynamicCompWithBindingsModule { + } + + + TestBed.configureTestingModule({ + declarations: [TestComp], + imports: [DynamicCompWithBindingsModule], + providers: [TEST_COMPONENT_RENDERER] + }); + const fixture = TestBed.createComponent(TestComp); + const {vcRef, cfResolver} = fixture.componentInstance; + fixture.detectChanges(); + + // Ivy inserts a comment for the root view container ref instance. This is not + // the case for view engine and we need to adjust the assertions. + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 2 : 1); + ivyEnabled && expect(containerEl !.childNodes[1].nodeType).toBe(Node.COMMENT_NODE); + + expect((containerEl !.childNodes[0] as Element).tagName).toBe('DIV'); + + vcRef.createComponent(cfResolver.resolveComponentFactory(DynamicCompWithBindings)); + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(containerEl !.childNodes[1].textContent).toBe('check count: 1'); + + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(containerEl !.childNodes[1].textContent).toBe('check count: 2'); + }); + + it('should create deep DOM tree immediately for dynamically created components', () => { + @Component({template: ``}) + class TestComp { + constructor(public vcRef: ViewContainerRef, public cfResolver: ComponentFactoryResolver) {} + } + + @Component({selector: 'child', template: `
    {{name}}
    `}) + class Child { + name = 'text'; + } + + @Component({selector: 'dynamic-cmpt-with-children', template: ``}) + class DynamicCompWithChildren { + } + + @NgModule({ + entryComponents: [DynamicCompWithChildren], + declarations: [DynamicCompWithChildren, Child] + }) + class DynamicCompWithChildrenModule { + } + + TestBed.configureTestingModule({ + declarations: [TestComp], + imports: [DynamicCompWithChildrenModule], + providers: [TEST_COMPONENT_RENDERER] + }); + + const fixture = TestBed.createComponent(TestComp); + const {vcRef, cfResolver} = fixture.componentInstance; + fixture.detectChanges(); + + // Ivy inserts a comment for the root view container ref instance. This is not + // the case for view engine and we need to adjust the assertions. + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 2 : 1); + ivyEnabled && expect(containerEl !.childNodes[1].nodeType).toBe(Node.COMMENT_NODE); + + expect((containerEl !.childNodes[0] as Element).tagName).toBe('DIV'); + + vcRef.createComponent(cfResolver.resolveComponentFactory(DynamicCompWithChildren)); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(getElementHtml(containerEl !.childNodes[1] as Element)) + .toBe('
    '); + + fixture.detectChanges(); + + expect(containerEl !.childNodes.length).toBe(ivyEnabled ? 3 : 2); + expect(getElementHtml(containerEl !.childNodes[1] as Element)) + .toBe(`
    text
    `); + }); + }); }); +@Component({ + template: ` + {{name}} +

    + `, +}) +class EmbeddedViewInsertionComp { +} + +@Directive({ + selector: '[vcref]', +}) +class VCRefDirective { + @Input() tplRef: TemplateRef|undefined; + @Input() name: string = ''; + + // Injecting the ViewContainerRef to create a dynamic container in which + // embedded views will be created + constructor( + public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver, + public elementRef: ElementRef) {} + + createView(s: string, index?: number): EmbeddedViewRef { + if (!this.tplRef) { + throw new Error('No template reference passed to directive.'); + } + + return this.vcref.createEmbeddedView(this.tplRef, {$implicit: s}, index); + } +} + +@Component({ + selector: `embedded-cmp-with-ngcontent`, + template: `
    ` +}) +class EmbeddedComponentWithNgContent { +} + +@NgModule({ + exports: [EmbeddedComponentWithNgContent], + entryComponents: [EmbeddedComponentWithNgContent], + declarations: [EmbeddedComponentWithNgContent], +}) +class EmbeddedComponentWithNgZoneModule { +} + @Component({ selector: 'view-container-ref-comp', template: ` @@ -292,7 +1754,7 @@ class ViewContainerRefComp { ` }) class ViewContainerRefApp { - @ViewChild(ViewContainerRefComp) vcrComp !: ViewContainerRefComp; + @ViewChild(ViewContainerRefComp, {static: false}) vcrComp !: ViewContainerRefComp; } @Directive({selector: '[structDir]'}) @@ -325,7 +1787,7 @@ class ConstructorDir { ` }) class ConstructorApp { - @ViewChild('foo') foo !: ElementRef; + @ViewChild('foo', {static: true}) foo !: ElementRef; } @Component({ @@ -337,5 +1799,5 @@ class ConstructorApp { ` }) class ConstructorAppWithQueries { - @ViewChild('foo') foo !: TemplateRef; + @ViewChild('foo', {static: true}) foo !: TemplateRef; } diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index 590c254b17..809464e825 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -345,7 +345,7 @@ const DEFAULT_COMPONENT_ID = '1'; ] }) class Cmp { - @ViewChild('element') + @ViewChild('element', {static: false}) element: any; exp: any = ''; } @@ -1433,7 +1433,7 @@ const DEFAULT_COMPONENT_ID = '1'; ])] }) class Cmp { - @ViewChild('green') public element: any; + @ViewChild('green', {static: false}) public element: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1770,7 +1770,7 @@ const DEFAULT_COMPONENT_ID = '1'; class Cmp { public exp: any; - @ViewChild('parent') public parentElement: any; + @ViewChild('parent', {static: false}) public parentElement: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1824,9 +1824,9 @@ const DEFAULT_COMPONENT_ID = '1'; public exp1: any; public exp2: any; - @ViewChild('parent') public parent: any; + @ViewChild('parent', {static: false}) public parent: any; - @ViewChild('child') public child: any; + @ViewChild('child', {static: false}) public child: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1881,11 +1881,11 @@ const DEFAULT_COMPONENT_ID = '1'; public exp1: any; public exp2: any; - @ViewChild('parent') public parent: any; + @ViewChild('parent', {static: false}) public parent: any; - @ViewChild('child1') public child1Elm: any; + @ViewChild('child1', {static: false}) public child1Elm: any; - @ViewChild('child2') public child2Elm: any; + @ViewChild('child2', {static: false}) public child2Elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2240,7 +2240,7 @@ const DEFAULT_COMPONENT_ID = '1'; [transition(':enter', [style({opacity: 0}), animate('1s', style({opacity: 1}))])])] }) class OuterCmp { - @ViewChild('inner') public inner: any; + @ViewChild('inner', {static: false}) public inner: any; public exp: any = null; update() { this.exp = 'go'; } @@ -3073,7 +3073,7 @@ const DEFAULT_COMPONENT_ID = '1'; exp: any = false; disableExp = false; - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: true}) public element: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -3231,7 +3231,7 @@ const DEFAULT_COMPONENT_ID = '1'; ] }) class Cmp { - @ViewChild('parent') public parentElm: any; + @ViewChild('parent', {static: false}) public parentElm: any; disableExp = false; exp = false; } @@ -3322,7 +3322,7 @@ const DEFAULT_COMPONENT_ID = '1'; ` }) class ParentCmp { - @ViewChild('child') public child: ChildCmp|null = null; + @ViewChild('child', {static: false}) public child: ChildCmp|null = null; disableExp = false; } @@ -3438,7 +3438,7 @@ const DEFAULT_COMPONENT_ID = '1'; ` }) class Cmp { - @ViewChild('container') public container: any; + @ViewChild('container', {static: false}) public container: any; disableExp = false; exp = ''; @@ -3661,7 +3661,7 @@ const DEFAULT_COMPONENT_ID = '1'; }); modifiedInIvy('FW-952 - Error recovery is handled differently in Ivy than VE') - .it('should continue to clean up DOM-related animation artificats even if a compiler-level error is thrown midway', + .it('should continue to clean up DOM-related animation artifacts even if a compiler-level error is thrown midway', () => { @Component({ selector: 'if-cmp', @@ -3684,7 +3684,7 @@ const DEFAULT_COMPONENT_ID = '1'; class Cmp { exp: any = false; - @ViewChild('contents') public contents: any; + @ViewChild('contents', {static: true}) public contents: any; } TestBed.configureTestingModule({declarations: [Cmp]}); diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index 6a4a339765..9ee2dfc0da 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -14,7 +14,6 @@ import {CommonModule} from '@angular/common'; import {Component, HostBinding, ViewChild} from '@angular/core'; import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {ivyEnabled} from '@angular/private/testing'; import {HostListener} from '../../src/metadata/directives'; @@ -888,7 +887,7 @@ import {HostListener} from '../../src/metadata/directives'; ] }) class Cmp { - @ViewChild('container') public container: any; + @ViewChild('container', {static: false}) public container: any; public items: any[] = []; } @@ -1202,9 +1201,9 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any = ''; public exp2: any = true; - @ViewChild('ancestor') public ancestorElm: any; + @ViewChild('ancestor', {static: false}) public ancestorElm: any; - @ViewChild('parent') public parentElm: any; + @ViewChild('parent', {static: false}) public parentElm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1281,9 +1280,9 @@ import {HostListener} from '../../src/metadata/directives'; public exp2: any = ''; public parentExp: any = true; - @ViewChild('ancestor') public ancestorElm: any; + @ViewChild('ancestor', {static: false}) public ancestorElm: any; - @ViewChild('parent') public parentElm: any; + @ViewChild('parent', {static: false}) public parentElm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1637,7 +1636,7 @@ import {HostListener} from '../../src/metadata/directives'; class ParentCmp { public exp: any; - @ViewChild('child') public child: any; + @ViewChild('child', {static: false}) public child: any; } @Component({ @@ -1686,7 +1685,7 @@ import {HostListener} from '../../src/metadata/directives'; class ParentCmp { public exp: any; - @ViewChild('child') public child: any; + @ViewChild('child', {static: true}) public child: any; } @Component({ @@ -1705,10 +1704,6 @@ import {HostListener} from '../../src/metadata/directives'; const fixture = TestBed.createComponent(ParentCmp); const cmp = fixture.componentInstance; - // In Ivy, change detection needs to run before the ViewQuery for cmp.child will resolve. - // Keeping this test enabled since we still want to test the animation logic in Ivy. - if (ivyEnabled) fixture.detectChanges(); - cmp.child.items = [4, 5, 6]; fixture.detectChanges(); @@ -1853,9 +1848,9 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any; public exp2: any; - @ViewChild('parent') public elm1: any; + @ViewChild('parent', {static: false}) public elm1: any; - @ViewChild('child') public elm2: any; + @ViewChild('child', {static: false}) public elm2: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1915,7 +1910,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp: any; public items: any[] = [0, 1, 2, 3, 4]; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -1985,7 +1980,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp: any; public items: any[] = [0, 1, 2, 3, 4]; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2038,7 +2033,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any; public exp2: any; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2108,7 +2103,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any; public exp2: any; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2161,7 +2156,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any; public exp2: any; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2213,7 +2208,7 @@ import {HostListener} from '../../src/metadata/directives'; public exp1: any; public exp2: any; - @ViewChild('parent') public elm: any; + @ViewChild('parent', {static: false}) public elm: any; } TestBed.configureTestingModule({declarations: [Cmp]}); @@ -2262,7 +2257,7 @@ import {HostListener} from '../../src/metadata/directives'; }) class ParentCmp { public exp: boolean = true; - @ViewChild('child') public childElm: any; + @ViewChild('child', {static: false}) public childElm: any; public childEvent: any; @@ -2698,7 +2693,7 @@ import {HostListener} from '../../src/metadata/directives'; class ParentCmp { public exp: any; - @ViewChild('child') public childCmp: any; + @ViewChild('child', {static: false}) public childCmp: any; } @Component({ @@ -2762,7 +2757,7 @@ import {HostListener} from '../../src/metadata/directives'; ` }) class ParentCmp { - @ViewChild('child') public childCmp: any; + @ViewChild('child', {static: false}) public childCmp: any; public exp: any; public log: string[] = []; @@ -2940,7 +2935,7 @@ import {HostListener} from '../../src/metadata/directives'; class ParentCmp { public exp: any; - @ViewChild('child') public childCmp: any; + @ViewChild('child', {static: false}) public childCmp: any; } @Component({ @@ -3018,13 +3013,13 @@ import {HostListener} from '../../src/metadata/directives'; class ParentCmp { public exp: any; - @ViewChild('child') public innerCmp: any; + @ViewChild('child', {static: false}) public innerCmp: any; } @Component( {selector: 'child-cmp', template: ''}) class ChildCmp { - @ViewChild('grandchild') public innerCmp: any; + @ViewChild('grandchild', {static: false}) public innerCmp: any; } @Component({ diff --git a/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts b/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts index 150b68d96f..5e6baa888d 100644 --- a/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts +++ b/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts @@ -11,7 +11,6 @@ import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_gr import {Component, ViewChild} from '@angular/core'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; -import {ivyEnabled} from '@angular/private/testing'; import {TestBed} from '../../testing'; @@ -166,7 +165,7 @@ import {TestBed} from '../../testing'; ] }) class Cmp { - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: false}) public element: any; public myAnimationExp = ''; } @@ -217,7 +216,7 @@ import {TestBed} from '../../testing'; ] }) class Cmp { - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: false}) public element: any; public myAnimationExp = ''; } @@ -280,7 +279,7 @@ import {TestBed} from '../../testing'; ] }) class Cmp { - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: true}) public element: any; public myAnimationExp = ''; } @@ -291,10 +290,6 @@ import {TestBed} from '../../testing'; const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; - // In Ivy, change detection needs to run before the ViewQuery for cmp.element will resolve. - // Keeping this test enabled since we still want to test the animation logic in Ivy. - if (ivyEnabled) fixture.detectChanges(); - const elm = cmp.element.nativeElement; const foo = elm.querySelector('.foo') as HTMLElement; @@ -332,7 +327,7 @@ import {TestBed} from '../../testing'; ] }) class Cmp { - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: true}) public element: any; public myAnimationExp = ''; } @@ -343,10 +338,6 @@ import {TestBed} from '../../testing'; const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; - // In Ivy, change detection needs to run before the ViewQuery for cmp.element will resolve. - // Keeping this test enabled since we still want to test the animation logic in Ivy. - if (ivyEnabled) fixture.detectChanges(); - const elm = cmp.element.nativeElement; expect(elm.style.getPropertyValue('display')).toEqual('table'); expect(elm.style.getPropertyValue('position')).toEqual('fixed'); diff --git a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts index 7096a28b8e..f21c76dc53 100644 --- a/packages/core/test/animation/animations_with_web_animations_integration_spec.ts +++ b/packages/core/test/animation/animations_with_web_animations_integration_spec.ts @@ -479,7 +479,7 @@ import {ivyEnabled} from '@angular/private/testing'; ] }) class Cmp { - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: true}) public element: any; public myAnimationExp = ''; } @@ -490,10 +490,6 @@ import {ivyEnabled} from '@angular/private/testing'; const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; - // In Ivy, change detection needs to run before the ViewQuery for cmp.element will resolve. - // Keeping this test enabled since we still want to test the animation logic in Ivy. - if (ivyEnabled) fixture.detectChanges(); - const elm = cmp.element.nativeElement; expect(elm.style.getPropertyValue('display')).toEqual('table'); expect(elm.style.getPropertyValue('position')).toEqual('fixed'); diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index 8a56680ec3..3974575acd 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -8,15 +8,16 @@ import {DOCUMENT} from '@angular/common'; import {ResourceLoader} from '@angular/compiler'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, InjectionToken, LOCALE_ID, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {ApplicationRef} from '@angular/core/src/application_ref'; import {ErrorHandler} from '@angular/core/src/error_handler'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; +import {getLocaleId} from '@angular/core/src/render3'; import {BrowserModule} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ivyEnabled} from '@angular/private/testing'; +import {onlyInIvy} from '@angular/private/testing'; import {NoopNgZone} from '../src/zone/ng_zone'; import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing'; @@ -326,6 +327,22 @@ class SomeComponent { expect(loadResourceSpy).toHaveBeenCalledTimes(1); expect(loadResourceSpy).toHaveBeenCalledWith('/test-template.html'); }); + + onlyInIvy('We only need to define `LOCALE_ID` for runtime i18n') + .it('should define `LOCALE_ID`', async() => { + @Component({ + selector: 'i18n-app', + templateUrl: '', + }) + class I18nComponent { + } + + const testModule = createModule( + {component: I18nComponent, providers: [{provide: LOCALE_ID, useValue: 'ro'}]}); + await defaultPlatform.bootstrapModule(testModule); + + expect(getLocaleId()).toEqual('ro'); + }); }); describe('bootstrapModuleFactory', () => { @@ -386,14 +403,14 @@ class SomeComponent { @Component({template: ''}) class ContainerComp { // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) + @ViewChild('vc', {read: ViewContainerRef, static: false}) vc !: ViewContainerRef; } @Component({template: 'Dynamic content'}) class EmbeddedViewComp { // TODO(issue/24571): remove '!'. - @ViewChild(TemplateRef) + @ViewChild(TemplateRef, {static: true}) tplRef !: TemplateRef; } @@ -447,10 +464,6 @@ class SomeComponent { const comp = TestBed.createComponent(EmbeddedViewComp); const appRef: ApplicationRef = TestBed.get(ApplicationRef); - // In Ivy, change detection needs to run before the ViewQuery for tplRef will resolve. - // Keeping this test enabled since we still want to test this destroy logic in Ivy. - if (ivyEnabled) comp.detectChanges(); - const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({}); appRef.attachView(embeddedViewRef); diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 412c559d91..5fd56e86c8 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -32,6 +32,15 @@ { "name": "DECLARATION_VIEW" }, + { + "name": "DEFAULT_BINDING_VALUE" + }, + { + "name": "DEFAULT_GUARD_MASK_VALUE" + }, + { + "name": "DEFAULT_SIZE_VALUE" + }, { "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" }, @@ -50,9 +59,6 @@ { "name": "FLAGS" }, - { - "name": "FactoryPrototype" - }, { "name": "HEADER_OFFSET" }, @@ -65,6 +71,9 @@ { "name": "INJECTOR_BLOOM_PARENT_SIZE" }, + { + "name": "MAP_BASED_ENTRY_PROP_NAME" + }, { "name": "MONKEY_PATCH_KEY_NAME" }, @@ -149,9 +158,21 @@ { "name": "ViewEncapsulation" }, + { + "name": "__global" + }, + { + "name": "__globalThis" + }, + { + "name": "__self" + }, { "name": "__values" }, + { + "name": "__window" + }, { "name": "_currentNamespace" }, @@ -164,6 +185,12 @@ { "name": "_selectedIndex" }, + { + "name": "_stylingMode" + }, + { + "name": "addBindingIntoContext" + }, { "name": "addComponentLogic" }, @@ -176,6 +203,12 @@ { "name": "allocStylingContext" }, + { + "name": "allocTStylingContext" + }, + { + "name": "allocateNewContextEntry" + }, { "name": "allocateOrUpdateDirectiveIntoContext" }, @@ -227,9 +260,6 @@ { "name": "createLView" }, - { - "name": "createNodeAtIndex" - }, { "name": "createRootComponent" }, @@ -242,6 +272,9 @@ { "name": "createTNode" }, + { + "name": "createTNodeAtIndex" + }, { "name": "createTView" }, @@ -314,6 +347,9 @@ { "name": "getCheckNoChangesMode" }, + { + "name": "getClassesContext" + }, { "name": "getClosureSafeProperty" }, @@ -326,6 +362,9 @@ { "name": "getContainerRenderParent" }, + { + "name": "getContext" + }, { "name": "getDirectiveDef" }, @@ -333,7 +372,7 @@ "name": "getElementDepthCount" }, { - "name": "getGlobal" + "name": "getGuardMask" }, { "name": "getHighestElementOrICUContainer" @@ -380,6 +419,9 @@ { "name": "getOrCreateNodeInjectorForNode" }, + { + "name": "getOrCreateTNode" + }, { "name": "getOrCreateTView" }, @@ -401,6 +443,15 @@ { "name": "getPreviousOrParentTNode" }, + { + "name": "getProp" + }, + { + "name": "getPropConfig" + }, + { + "name": "getPropValuesStartPosition" + }, { "name": "getRenderFlags" }, @@ -416,12 +467,18 @@ { "name": "getSelectedIndex" }, + { + "name": "getStylesContext" + }, { "name": "getStylingContextFromLView" }, { "name": "getTNode" }, + { + "name": "getValuesCount" + }, { "name": "hasClassInput" }, @@ -578,6 +635,12 @@ { "name": "refreshDynamicEmbeddedViews" }, + { + "name": "registerBinding" + }, + { + "name": "registerInitialStylingIntoContext" + }, { "name": "registerPostOrderHooks" }, @@ -611,6 +674,9 @@ { "name": "resolveDirectives" }, + { + "name": "runtimeIsNewStylingInUse" + }, { "name": "saveNameToExportMap" }, @@ -635,6 +701,9 @@ { "name": "setCurrentQueryIndex" }, + { + "name": "setGuardMask" + }, { "name": "setHostBindings" }, @@ -651,7 +720,7 @@ "name": "setInputsFromAttrs" }, { - "name": "setIsParent" + "name": "setIsNotParent" }, { "name": "setNodeStylingTemplate" diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index d6157ce51b..e11a3baabb 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -41,9 +41,6 @@ { "name": "FLAGS" }, - { - "name": "FactoryPrototype" - }, { "name": "HEADER_OFFSET" }, @@ -128,9 +125,21 @@ { "name": "ViewEncapsulation" }, + { + "name": "__global" + }, + { + "name": "__globalThis" + }, + { + "name": "__self" + }, { "name": "__values" }, + { + "name": "__window" + }, { "name": "_global" }, @@ -176,9 +185,6 @@ { "name": "createLView" }, - { - "name": "createNodeAtIndex" - }, { "name": "createRootComponent" }, @@ -191,6 +197,9 @@ { "name": "createTNode" }, + { + "name": "createTNodeAtIndex" + }, { "name": "createTView" }, @@ -254,9 +263,6 @@ { "name": "getDirectiveDef" }, - { - "name": "getGlobal" - }, { "name": "getHighestElementOrICUContainer" }, @@ -290,6 +296,9 @@ { "name": "getOrCreateNodeInjectorForNode" }, + { + "name": "getOrCreateTNode" + }, { "name": "getOrCreateTView" }, @@ -471,7 +480,7 @@ "name": "setInjectImplementation" }, { - "name": "setIsParent" + "name": "setIsNotParent" }, { "name": "setPreviousOrParentTNode" diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 057315aa95..ac75847f48 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -95,9 +95,6 @@ { "name": "_currentInjector" }, - { - "name": "_global" - }, { "name": "catchInjectorError" }, @@ -120,7 +117,7 @@ "name": "getClosureSafeProperty" }, { - "name": "getGlobal" + "name": "getInheritedInjectableDef" }, { "name": "getInjectableDef" @@ -131,6 +128,9 @@ { "name": "getNullInjector" }, + { + "name": "getUndecoratedInjectableFactory" + }, { "name": "hasDeps" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 96e4a1e3c1..b96cca104a 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -47,6 +47,15 @@ { "name": "DECLARATION_VIEW" }, + { + "name": "DEFAULT_BINDING_VALUE" + }, + { + "name": "DEFAULT_GUARD_MASK_VALUE" + }, + { + "name": "DEFAULT_SIZE_VALUE" + }, { "name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX" }, @@ -83,9 +92,6 @@ { "name": "FLAGS" }, - { - "name": "FactoryPrototype" - }, { "name": "HEADER_OFFSET" }, @@ -110,6 +116,9 @@ { "name": "IterableDiffers" }, + { + "name": "MAP_BASED_ENTRY_PROP_NAME" + }, { "name": "MIN_DIRECTIVE_ID" }, @@ -206,6 +215,12 @@ { "name": "SANITIZER" }, + { + "name": "STYLING_INDEX_FOR_MAP_BINDING" + }, + { + "name": "STYLING_INDEX_START_VALUE" + }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -215,6 +230,9 @@ { "name": "SWITCH_VIEW_CONTAINER_REF_FACTORY" }, + { + "name": "SecurityContext" + }, { "name": "SkipSelf" }, @@ -284,15 +302,30 @@ { "name": "__forward_ref__" }, + { + "name": "__global" + }, + { + "name": "__globalThis" + }, { "name": "__read" }, + { + "name": "__self" + }, { "name": "__spread" }, { "name": "__values" }, + { + "name": "__window" + }, + { + "name": "_activeStylingMapApplyFn" + }, { "name": "_c0" }, @@ -380,6 +413,12 @@ { "name": "_selectedIndex" }, + { + "name": "_stylingMode" + }, + { + "name": "_stylingProp" + }, { "name": "_symbolIterator" }, @@ -392,6 +431,9 @@ { "name": "activeDirectiveSuperClassHeight" }, + { + "name": "addBindingIntoContext" + }, { "name": "addComponentLogic" }, @@ -416,21 +458,39 @@ { "name": "allocStylingContext" }, + { + "name": "allocTStylingContext" + }, + { + "name": "allocateNewContextEntry" + }, { "name": "allocateOrUpdateDirectiveIntoContext" }, { "name": "allowFlush" }, + { + "name": "allowStylingFlush" + }, { "name": "allowValueChange" }, { "name": "appendChild" }, + { + "name": "applyClasses" + }, { "name": "applyOnCreateInstructions" }, + { + "name": "applyStyles" + }, + { + "name": "applyStyling" + }, { "name": "assertTemplate" }, @@ -479,6 +539,12 @@ { "name": "checkView" }, + { + "name": "classProp" + }, + { + "name": "classesBitMask" + }, { "name": "cleanUpView" }, @@ -518,9 +584,6 @@ { "name": "createLView" }, - { - "name": "createNodeAtIndex" - }, { "name": "createRootComponent" }, @@ -533,6 +596,9 @@ { "name": "createTNode" }, + { + "name": "createTNodeAtIndex" + }, { "name": "createTView" }, @@ -545,6 +611,12 @@ { "name": "createViewBlueprint" }, + { + "name": "currentClassIndex" + }, + { + "name": "currentStyleIndex" + }, { "name": "decreaseElementDepthCount" }, @@ -554,6 +626,12 @@ { "name": "defaultScheduler" }, + { + "name": "deferBindingRegistration" + }, + { + "name": "deferredBindingQueue" + }, { "name": "destroyLView" }, @@ -641,6 +719,9 @@ { "name": "findViaComponent" }, + { + "name": "flushDeferredBindings" + }, { "name": "flushQueue" }, @@ -662,12 +743,24 @@ { "name": "getActiveDirectiveStylingIndex" }, + { + "name": "getActiveDirectiveStylingIndex" + }, { "name": "getActiveDirectiveSuperClassDepth" }, + { + "name": "getActiveDirectiveSuperClassHeight" + }, { "name": "getBeforeNodeForView" }, + { + "name": "getBindingNameFromIndex" + }, + { + "name": "getBindingValue" + }, { "name": "getBindingsEnabled" }, @@ -677,6 +770,9 @@ { "name": "getCheckNoChangesMode" }, + { + "name": "getClassesContext" + }, { "name": "getCleanup" }, @@ -692,12 +788,24 @@ { "name": "getComponentViewByInstance" }, + { + "name": "getConfig" + }, { "name": "getContainerRenderParent" }, + { + "name": "getContext" + }, { "name": "getContextLView" }, + { + "name": "getCurrentOrLViewSanitizer" + }, + { + "name": "getCurrentStyleSanitizer" + }, { "name": "getDebugContext" }, @@ -714,7 +822,7 @@ "name": "getErrorLogger" }, { - "name": "getGlobal" + "name": "getGuardMask" }, { "name": "getHighestElementOrICUContainer" @@ -779,6 +887,9 @@ { "name": "getNativeByTNode" }, + { + "name": "getNativeFromLView" + }, { "name": "getNodeInjectable" }, @@ -788,6 +899,9 @@ { "name": "getOrCreateNodeInjectorForNode" }, + { + "name": "getOrCreateTNode" + }, { "name": "getOrCreateTView" }, @@ -836,12 +950,24 @@ { "name": "getProp" }, + { + "name": "getProp" + }, + { + "name": "getPropConfig" + }, + { + "name": "getPropValuesStartPosition" + }, { "name": "getRenderFlags" }, { "name": "getRenderParent" }, + { + "name": "getRenderer" + }, { "name": "getRootContext" }, @@ -857,12 +983,18 @@ { "name": "getStyleSanitizer" }, + { + "name": "getStylesContext" + }, { "name": "getStylingContext" }, { "name": "getStylingContextFromLView" }, + { + "name": "getStylingMapsSyncFn" + }, { "name": "getSymbolIterator" }, @@ -881,6 +1013,9 @@ { "name": "getValue" }, + { + "name": "getValuesCount" + }, { "name": "handleError" }, @@ -905,6 +1040,9 @@ { "name": "hasValueChanged" }, + { + "name": "hasValueChanged" + }, { "name": "hyphenate" }, @@ -923,15 +1061,15 @@ { "name": "initNodeFlags" }, + { + "name": "initStyling" + }, { "name": "initializeStaticContext" }, { "name": "initializeTNodeInputs" }, - { - "name": "initstyling" - }, { "name": "injectElementRef" }, @@ -983,6 +1121,9 @@ { "name": "isContextDirty" }, + { + "name": "isContextLocked" + }, { "name": "isCreationMode" }, @@ -1031,9 +1172,15 @@ { "name": "isRootView" }, + { + "name": "isSanitizationRequired" + }, { "name": "isStylingContext" }, + { + "name": "isStylingValueDefined" + }, { "name": "iterateListLike" }, @@ -1052,6 +1199,9 @@ { "name": "locateHostElement" }, + { + "name": "lockContext" + }, { "name": "looseIdentical" }, @@ -1103,6 +1253,9 @@ { "name": "noSideEffects" }, + { + "name": "normalizeBitMaskValue" + }, { "name": "patchContextWithStaticAttrs" }, @@ -1148,9 +1301,15 @@ { "name": "refreshDynamicEmbeddedViews" }, + { + "name": "registerBinding" + }, { "name": "registerHostDirective" }, + { + "name": "registerInitialStylingIntoContext" + }, { "name": "registerMultiMapEntry" }, @@ -1202,6 +1361,15 @@ { "name": "resolveForwardRef" }, + { + "name": "runtimeAllowOldStyling" + }, + { + "name": "runtimeIsNewStylingInUse" + }, + { + "name": "sanitizeUsingSanitizerObject" + }, { "name": "saveNameToExportMap" }, @@ -1232,6 +1400,12 @@ { "name": "setClass" }, + { + "name": "setClass" + }, + { + "name": "setConfig" + }, { "name": "setContextDirty" }, @@ -1244,12 +1418,18 @@ { "name": "setCurrentQueryIndex" }, + { + "name": "setCurrentStyleSanitizer" + }, { "name": "setDirty" }, { "name": "setFlag" }, + { + "name": "setGuardMask" + }, { "name": "setHostBindings" }, @@ -1266,7 +1446,7 @@ "name": "setInputsFromAttrs" }, { - "name": "setIsParent" + "name": "setIsNotParent" }, { "name": "setNodeStylingTemplate" @@ -1292,6 +1472,9 @@ { "name": "setStyle" }, + { + "name": "setStyle" + }, { "name": "setTNodeAndViewData" }, @@ -1316,9 +1499,18 @@ { "name": "stringifyForError" }, + { + "name": "stylesBitMask" + }, + { + "name": "stylingApply" + }, { "name": "stylingContext" }, + { + "name": "stylingInit" + }, { "name": "syncViewWithBlueprint" }, @@ -1334,15 +1526,30 @@ { "name": "unwrapRNode" }, + { + "name": "updateBindingData" + }, + { + "name": "updateClassBinding" + }, { "name": "updateClassProp" }, + { + "name": "updateContextDirectiveIndex" + }, { "name": "updateContextWithBindings" }, + { + "name": "updateLastDirectiveIndex" + }, { "name": "updateSingleStylingValue" }, + { + "name": "updateStyleBinding" + }, { "name": "valueExists" }, @@ -1429,5 +1636,11 @@ }, { "name": "ɵɵtextBinding" + }, + { + "name": "ɵɵtextInterpolate" + }, + { + "name": "ɵɵtextInterpolate1" } ] \ No newline at end of file diff --git a/packages/core/test/debug/debug_node_spec.ts b/packages/core/test/debug/debug_node_spec.ts index 150c20c60a..cbec33a9ca 100644 --- a/packages/core/test/debug/debug_node_spec.ts +++ b/packages/core/test/debug/debug_node_spec.ts @@ -7,7 +7,7 @@ */ -import {Component, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {Component, DebugNode, Directive, ElementRef, EmbeddedViewRef, EventEmitter, HostBinding, Injectable, Input, NO_ERRORS_SCHEMA, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -160,7 +160,7 @@ class BankAccount { ` }) class SimpleContentComp { - @ViewChild('content') content !: ElementRef; + @ViewChild('content', {static: false}) content !: ElementRef; } @Component({ @@ -678,5 +678,63 @@ class TestCmptWithPropBindings { expect(divB.nativeElement.getAttribute('id')).toBe('b'); }); + it('should be an instance of DebugNode', () => { + fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + expect(fixture.debugElement).toBeAnInstanceOf(DebugNode); + }); + + it('should return the same element when queried twice', () => { + fixture = TestBed.createComponent(ParentComp); + fixture.detectChanges(); + + const childTestElsFirst = fixture.debugElement.queryAll(By.css('child-comp')); + const childTestElsSecond = fixture.debugElement.queryAll(By.css('child-comp')); + + expect(childTestElsFirst.length).toBe(1); + expect(childTestElsSecond[0]).toBe(childTestElsFirst[0]); + }); + + it('should not query the descendants of a sibling node', () => { + @Component({ + selector: 'my-comp', + template: ` +
    +

    + span.1 + span.2 +

    +

    + span.3 + span.4 +

    +
    +
    +

    + span.5 + span.6 +

    +

    + span.7 + span.8 +

    +
    + ` + }) + class MyComp { + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const firstDiv = fixture.debugElement.query(By.css('div')); + const firstDivChildren = firstDiv.queryAll(By.css('span')); + + expect(firstDivChildren.map(child => child.nativeNode.textContent.trim())).toEqual([ + 'span.1', 'span.2', 'span.3', 'span.4' + ]); + }); + }); } diff --git a/packages/core/test/di/r3_injector_spec.ts b/packages/core/test/di/r3_injector_spec.ts index 75907a1aee..f60301a0f2 100644 --- a/packages/core/test/di/r3_injector_spec.ts +++ b/packages/core/test/di/r3_injector_spec.ts @@ -217,6 +217,31 @@ describe('InjectorDef-based createInjector()', () => { }); } + class MultiProviderA { + static ngInjectorDef = ɵɵdefineInjector({ + factory: () => new MultiProviderA(), + providers: [{provide: LOCALE, multi: true, useValue: 'A'}], + }); + } + + class MultiProviderB { + static ngInjectorDef = ɵɵdefineInjector({ + factory: () => new MultiProviderB(), + providers: [{provide: LOCALE, multi: true, useValue: 'B'}], + }); + } + + class WithProvidersTest { + static ngInjectorDef = ɵɵdefineInjector({ + factory: () => new WithProvidersTest(), + imports: [ + {ngModule: MultiProviderA, providers: [{provide: LOCALE, multi: true, useValue: 'C'}]}, + MultiProviderB + ], + providers: [], + }); + } + let injector: Injector; beforeEach(() => { @@ -274,6 +299,11 @@ describe('InjectorDef-based createInjector()', () => { expect(instance.locale).toEqual(['en', 'es']); }); + it('should process "InjectionTypeWithProviders" providers after imports injection type', () => { + injector = createInjector(WithProvidersTest); + expect(injector.get(LOCALE)).toEqual(['A', 'B', 'C']); + }); + it('injects an injector with dependencies', () => { const instance = injector.get(InjectorWithDep); expect(instance instanceof InjectorWithDep); diff --git a/packages/core/test/i18n_integration_spec.ts b/packages/core/test/i18n_integration_spec.ts deleted file mode 100644 index af09a356a7..0000000000 --- a/packages/core/test/i18n_integration_spec.ts +++ /dev/null @@ -1,640 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; -import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {onlyInIvy} from '@angular/private/testing'; - -@Directive({ - selector: '[tplRef]', -}) -class DirectiveWithTplRef { - constructor(public vcRef: ViewContainerRef, public tplRef: TemplateRef<{}>) {} - ngOnInit() { this.vcRef.createEmbeddedView(this.tplRef, {}); } -} - -@Component({selector: 'my-comp', template: ''}) -class MyComp { - name = 'John'; - items = ['1', '2', '3']; - obj = {a: {b: 'value'}}; - visible = true; - age = 20; - count = 2; - otherLabel = 'other label'; - clicks = 0; - - onClick() { this.clicks++; } -} - -const TRANSLATIONS: any = { - 'one': 'un', - 'two': 'deux', - 'more than two': 'plus que deux', - 'ten': 'dix', - 'twenty': 'vingt', - 'other': 'autres', - 'Hello': 'Bonjour', - 'Hello {$interpolation}': 'Bonjour {$interpolation}', - 'Bye': 'Au revoir', - 'Item {$interpolation}': 'Article {$interpolation}', - '\'Single quotes\' and "Double quotes"': '\'Guillemets simples\' et "Guillemets doubles"', - 'My logo': 'Mon logo', - '{$interpolation} - {$interpolation_1}': '{$interpolation} - {$interpolation_1} (fr)', - '{$startTagSpan}My logo{$tagImg}{$closeTagSpan}': - '{$startTagSpan}Mon logo{$tagImg}{$closeTagSpan}', - '{$startTagNgTemplate} Hello {$closeTagNgTemplate}{$startTagNgContainer} Bye {$closeTagNgContainer}': - '{$startTagNgTemplate} Bonjour {$closeTagNgTemplate}{$startTagNgContainer} Au revoir {$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$startTagNgTemplate}{$startTagSpan}Hello{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Hello{$closeTagSpan}{$closeTagNgContainer}': - '{$startTagNgTemplate}{$startTagSpan}Bonjour{$closeTagSpan}{$closeTagNgTemplate}{$startTagNgContainer}{$startTagSpan_1}Bonjour{$closeTagSpan}{$closeTagNgContainer}', - '{$startTagSpan} Hello - 1 {$closeTagSpan}{$startTagSpan_1} Hello - 2 {$startTagSpan_1} Hello - 3 {$startTagSpan_1} Hello - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Hello - 5 {$closeTagSpan}': - '{$startTagSpan} Bonjour - 1 {$closeTagSpan}{$startTagSpan_1} Bonjour - 2 {$startTagSpan_1} Bonjour - 3 {$startTagSpan_1} Bonjour - 4 {$closeTagSpan}{$closeTagSpan}{$closeTagSpan}{$startTagSpan} Bonjour - 5 {$closeTagSpan}', - '{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}': - '{VAR_SELECT, select, 10 {dix} 20 {vingt} other {autres}}', - '{VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}': - '{VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}', - '{VAR_SELECT, select, 10 {10 - {$startBoldText}ten{$closeBoldText}} 20 {20 - {$startItalicText}twenty{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}other{$closeUnderlinedText}{$closeTagDiv}}}': - '{VAR_SELECT, select, 10 {10 - {$startBoldText}dix{$closeBoldText}} 20 {20 - {$startItalicText}vingt{$closeItalicText}} other {{$startTagDiv}{$startUnderlinedText}autres{$closeUnderlinedText}{$closeTagDiv}}}', - '{VAR_SELECT_2, select, 10 {ten - {VAR_SELECT, select, 1 {one} 2 {two} other {more than two}}} 20 {twenty - {VAR_SELECT_1, select, 1 {one} 2 {two} other {more than two}}} other {other}}': - '{VAR_SELECT_2, select, 10 {dix - {VAR_SELECT, select, 1 {un} 2 {deux} other {plus que deux}}} 20 {vingt - {VAR_SELECT_1, select, 1 {un} 2 {deux} other {plus que deux}}} other {autres}}', - '{$startTagNgTemplate}{$startTagDiv_1}{$startTagDiv}{$startTagSpan}Content{$closeTagSpan}{$closeTagDiv}{$closeTagDiv}{$closeTagNgTemplate}': - '{$startTagNgTemplate}Contenu{$closeTagNgTemplate}' -}; - -const getFixtureWithOverrides = (overrides = {}) => { - TestBed.overrideComponent(MyComp, {set: overrides}); - const fixture = TestBed.createComponent(MyComp); - fixture.detectChanges(); - return fixture; -}; - -onlyInIvy('Ivy i18n logic').describe('i18n', function() { - - beforeEach(() => { - ɵi18nConfigureLocalize({translations: TRANSLATIONS}); - TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]}); - }); - - describe('attributes', () => { - it('should translate static attributes', () => { - const title = 'Hello'; - const template = `
    `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour'); - }); - - it('should support interpolation', () => { - const title = 'Hello {{ name }}'; - const template = `
    `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const title = 'Hello {% name %}'; - const template = `
    `; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - - it('should correctly bind to context in nested template', () => { - const title = 'Item {{ id }}'; - const template = ` -
    -
    -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect((child as any).innerHTML).toBe(`
    `); - } - }); - - it('should work correctly when placed on i18n root node', () => { - const title = 'Hello {{ name }}'; - const content = 'Hello'; - const template = ` -
    ${content}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - expect(element).toHaveText('Bonjour'); - }); - - it('should add i18n attributes on self-closing tags', () => { - const title = 'Hello {{ name }}'; - const template = ``; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.title).toBe('Bonjour John'); - }); - }); - - describe('nested nodes', () => { - it('should handle static content', () => { - const content = 'Hello'; - const template = `
    ${content}
    `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour'); - }); - - it('should support interpolation', () => { - const content = 'Hello {{ name }}'; - const template = `
    ${content}
    `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolation with custom interpolation config', () => { - const content = 'Hello {% name %}'; - const template = `
    ${content}
    `; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should support interpolations with complex expressions', () => { - const template = `
    {{ name | uppercase }} - {{ obj?.a?.b }}
    `; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('JOHN - value (fr)'); - }); - - it('should properly escape quotes in content', () => { - const content = `'Single quotes' and "Double quotes"`; - const template = `
    ${content}
    `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('\'Guillemets simples\' et "Guillemets doubles"'); - }); - - it('should correctly bind to context in nested template', () => { - const content = 'Item {{ id }}'; - const template = ` -
    -
    ${content}
    -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child).toHaveText(`Article ${i + 1}`); - } - }); - - it('should handle i18n attributes inside i18n section', () => { - const title = 'Hello {{ name }}'; - const template = ` -
    -
    -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const content = `
    `; - expect(element.innerHTML).toBe(content); - }); - - it('should handle i18n blocks in nested templates', () => { - const content = 'Hello {{ name }}'; - const template = ` -
    -
    ${content}
    -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.children[0]).toHaveText('Bonjour John'); - }); - - it('should ignore i18n attributes on self-closing tags', () => { - const template = ''; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element.innerHTML).toBe(template.replace(' i18n', '')); - }); - - it('should handle i18n attribute with directives', () => { - const content = 'Hello {{ name }}'; - const template = ` -
    ${content}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should work correctly with event listeners', () => { - const content = 'Hello {{ name }}'; - const template = ` -
    ${content}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const instance = fixture.componentInstance; - - expect(element).toHaveText('Bonjour John'); - expect(instance.clicks).toBe(0); - - element.click(); - expect(instance.clicks).toBe(1); - }); - }); - - describe('ng-container and ng-template support', () => { - it('should handle single translation message within ng-container', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('Bonjour John'); - }); - - it('should handle single translation message within ng-template', () => { - const content = 'Hello {{ name }}'; - const template = ` - ${content} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('Bonjour John'); - }); - - it('should be able to act as child elements inside i18n block (plain text content)', () => { - const hello = 'Hello'; - const bye = 'Bye'; - const template = ` -
    - - ${hello} - - - ${bye} - -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element.textContent.replace(/\s+/g, ' ').trim()).toBe('Bonjour Au revoir'); - }); - - it('should be able to act as child elements inside i18n block (text + tags)', () => { - const content = 'Hello'; - const template = ` -
    - - ${content} - - - ${content} - -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - expect(spans[i]).toHaveText('Bonjour'); - } - }); - - it('should be able to handle deep nested levels with templates', () => { - const content = 'Hello'; - const template = ` -
    - - ${content} - 1 - - - ${content} - 2 - - ${content} - 3 - - ${content} - 4 - - - - - ${content} - 5 - -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - expect(spans[i].innerHTML).toContain(`Bonjour - ${i + 1}`); - } - }); - - it('should handle self-closing tags as content', () => { - const label = 'My logo'; - const content = `${label}`; - const template = ` - - ${content} - - - ${content} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - const spans = element.getElementsByTagName('span'); - for (let i = 0; i < spans.length; i++) { - const child = spans[i]; - expect(child).toHaveText('Mon logo'); - } - }); - }); - - describe('ICU logic', () => { - it('should handle single ICUs', () => { - const template = ` -
    {age, select, 10 {ten} 20 {twenty} other {other}}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICU-only templates', () => { - const template = ` - {age, select, 10 {ten} 20 {twenty} other {other}} - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support ICUs generated outside of i18n blocks', () => { - const template = ` -
    {age, select, 10 {ten} 20 {twenty} other {other}}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should support interpolation', () => { - const template = ` -
    {age, select, 10 {ten} other {{{ otherLabel }}}}
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should support interpolation with custom interpolation config', () => { - const template = ` -
    {age, select, 10 {ten} other {{% otherLabel %}}}
    - `; - const interpolation = ['{%', '%}'] as[string, string]; - const fixture = getFixtureWithOverrides({template, interpolation}); - - const element = fixture.nativeElement; - expect(element).toHaveText(fixture.componentInstance.otherLabel); - }); - - it('should handle ICUs with HTML tags inside', () => { - const template = ` -
    - {age, select, 10 {10 - ten} 20 {20 - twenty} other {
    other
    }} -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const italicTags = element.getElementsByTagName('i'); - expect(italicTags.length).toBe(1); - expect(italicTags[0].innerHTML).toBe('vingt'); - }); - - it('should handle multiple ICUs in one block', () => { - const template = ` -
    - {age, select, 10 {ten} 20 {twenty} other {other}} - - {count, select, 1 {one} 2 {two} other {more than two}} -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle multiple ICUs in one i18n block wrapped in HTML elements', () => { - const template = ` -
    - - {age, select, 10 {ten} 20 {twenty} other {other}} - - - {count, select, 1 {one} 2 {two} other {more than two}} - -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(2); - expect(spans[0]).toHaveText('vingt'); - expect(spans[1]).toHaveText('deux'); - }); - - it('should handle ICUs inside a template in i18n block', () => { - const template = ` -
    - - {age, select, 10 {ten} 20 {twenty} other {other}} - -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - const spans = element.getElementsByTagName('span'); - expect(spans.length).toBe(1); - expect(spans[0]).toHaveText('vingt'); - }); - - it('should handle nested icus', () => { - const template = ` -
    - {age, select, - 10 {ten - {count, select, 1 {one} 2 {two} other {more than two}}} - 20 {twenty - {count, select, 1 {one} 2 {two} other {more than two}}} - other {other}} -
    - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement.firstChild; - expect(element).toHaveText('vingt - deux'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - - it('should handle ICUs inside ', () => { - const template = ` - - {age, select, 10 {ten} 20 {twenty} other {other}} - - `; - const fixture = getFixtureWithOverrides({template}); - - const element = fixture.nativeElement; - expect(element).toHaveText('vingt'); - }); - }); - - describe('queries', () => { - function toHtml(element: Element): string { - return element.innerHTML.replace(/\sng-reflect-\S*="[^"]*"/g, '') - .replace(//g, ''); - } - - it('detached nodes should still be part of query', () => { - const template = ` - - -
    -
    - Content -
    -
    -
    -
    - `; - - @Directive({selector: '[text]', inputs: ['text'], exportAs: 'textDir'}) - class TextDirective { - // TODO(issue/24571): remove '!'. - text !: string; - constructor() {} - } - - @Component({selector: 'div-query', template: ''}) - class DivQuery { - // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) template !: TemplateRef; - - // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) - vc !: ViewContainerRef; - - // TODO(issue/24571): remove '!'. - @ContentChildren(TextDirective, {descendants: true}) - query !: QueryList; - - create() { this.vc.createEmbeddedView(this.template); } - - destroy() { this.vc.clear(); } - } - - TestBed.configureTestingModule({declarations: [TextDirective, DivQuery]}); - const fixture = getFixtureWithOverrides({template}); - const q = fixture.debugElement.children[0].references.q; - expect(q.query.length).toEqual(0); - - // Create embedded view - q.create(); - fixture.detectChanges(); - expect(q.query.length).toEqual(1); - expect(toHtml(fixture.nativeElement)) - .toEqual(`Contenu`); - - // Disable ng-if - fixture.componentInstance.visible = false; - fixture.detectChanges(); - expect(q.query.length).toEqual(0); - expect(toHtml(fixture.nativeElement)) - .toEqual(`Contenu`); - }); - }); - - it('should handle multiple i18n sections', () => { - const template = ` -
    Section 1
    -
    Section 2
    -
    Section 3
    - `; - const fixture = getFixtureWithOverrides({template}); - expect(fixture.nativeElement.innerHTML) - .toBe('
    Section 1
    Section 2
    Section 3
    '); - }); - - it('should handle multiple i18n sections inside of *ngFor', () => { - const template = ` -
      -
    • Section 1
    • -
    • Section 2
    • -
    • Section 3
    • -
    - `; - const fixture = getFixtureWithOverrides({template}); - const element = fixture.nativeElement; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - expect(child.innerHTML).toBe(`
  • Section 1
  • Section 2
  • Section 3
  • `); - } - }); -}); diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 7d1322f772..0602fb671c 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -1356,9 +1356,9 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ class Comp { name = 'Tom'; // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) vc !: ViewContainerRef; + @ViewChild('vc', {read: ViewContainerRef, static: true}) vc !: ViewContainerRef; // TODO(issue/24571): remove '!'. - @ViewChild(TemplateRef) template !: TemplateRef; + @ViewChild(TemplateRef, {static: true}) template !: TemplateRef; } TestBed.configureTestingModule({declarations: [Comp]}); @@ -1399,7 +1399,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ }) class OuterComp { // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) + @ContentChild(TemplateRef, {static: true}) tpl !: TemplateRef; constructor(public cdRef: ChangeDetectorRef) {} @@ -1413,7 +1413,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ }) class InnerComp { // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) + @ContentChild(TemplateRef, {static: true}) tpl !: TemplateRef; // TODO(issue/24571): remove '!'. diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 2687a54e6a..b6bb694786 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1522,7 +1522,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('INPUT'); expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); expect((c.injector).get).toBeTruthy(); - expect(c.context).toBe(fixture.componentInstance); + expect(c.context).toEqual(fixture.componentInstance); expect(c.references['local']).toBeDefined(); } }); @@ -1567,7 +1567,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().nodeName(c.renderNode).toUpperCase()).toEqual('SPAN'); expect(getDOM().nodeName(c.componentRenderElement).toUpperCase()).toEqual('DIV'); expect((c.injector).get).toBeTruthy(); - expect(c.context).toBe(fixture.componentInstance); + expect(c.context).toEqual(fixture.componentInstance); expect(c.references['local']).toBeDefined(); })); }); diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index 4500bd26df..ee6eee8241 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, Self, Type, forwardRef, getModuleFactory, ɵivyEnabled as ivyEnabled} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, InjectionToken, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, Self, Type, forwardRef, getModuleFactory, ɵivyEnabled as ivyEnabled, ɵɵdefineNgModule as defineNgModule} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {ɵɵInjectableDef, ɵɵdefineInjectable} from '@angular/core/src/di/interface/defs'; import {getNgModuleDef} from '@angular/core/src/render3/definition'; @@ -17,7 +17,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers'; import {modifiedInIvy, obsoleteInIvy, onlyInIvy} from '@angular/private/testing'; import {InternalNgModuleRef, NgModuleFactory} from '../../src/linker/ng_module_factory'; -import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader'; +import {clearModulesForTest} from '../../src/linker/ng_module_factory_registration'; import {stringify} from '../../src/util/stringify'; class Engine {} @@ -142,7 +142,7 @@ function declareTests(config?: {useJit: boolean}) { // may face a problem where previously compiled defs available to a given // Component/Directive are cached in TView and may become stale (in case any of these defs // gets recompiled). In order to avoid this problem, we force fresh TView to be created. - componentDef.template.ngPrivateData = null; + componentDef.TView = null; } const ngModule = createModule(moduleType, injector); @@ -327,6 +327,40 @@ function declareTests(config?: {useJit: boolean}) { createModule(SomeOtherModule); }).toThrowError(/Duplicate module registered/); }); + + it('should not throw immediately if two modules have the same id', () => { + expect(() => { + @NgModule({id: 'some-module'}) + class ModuleA { + } + + @NgModule({id: 'some-module'}) + class ModuleB { + } + }).not.toThrow(); + }); + + onlyInIvy('VE does not allow use of NgModuleFactory without importing the .ngfactory') + .it('should register a module even if not importing the .ngfactory file or calling create()', + () => { + class ChildModule { + static ngModuleDef = defineNgModule({ + type: ChildModule, + id: 'child', + }); + } + + class Module { + static ngModuleDef = defineNgModule({ + type: Module, + id: 'test', + imports: [ChildModule], + }); + } + + createModuleFactory(ChildModule); + expect(getModuleFactory('child')).toBeAnInstanceOf(NgModuleFactory); + }); }); describe('entryComponents', () => { diff --git a/packages/core/test/linker/projection_integration_spec.ts b/packages/core/test/linker/projection_integration_spec.ts index d55447c3ea..8e528141fd 100644 --- a/packages/core/test/linker/projection_integration_spec.ts +++ b/packages/core/test/linker/projection_integration_spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {CommonModule} from '@angular/common'; import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, Injector, Input, NO_ERRORS_SCHEMA, NgModule, OnInit, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; @@ -111,13 +112,48 @@ describe('projection', () => { expect(main.nativeElement).toHaveText('(A, BC)'); }); - modifiedInIvy( - 'FW-886: `projectableNodes` passed to a componentFactory should be in the order of declaration') - .it('should support passing projectable nodes via factory function', () => { + it('should support passing projectable nodes via factory function', () => { + @Component({ + selector: 'multiple-content-tags', + template: '(, )', + }) + class MultipleContentTagsComponent { + } + @NgModule({ + declarations: [MultipleContentTagsComponent], + entryComponents: [MultipleContentTagsComponent], + schemas: [NO_ERRORS_SCHEMA], + }) + class MyModule { + } + + TestBed.configureTestingModule({imports: [MyModule]}); + const injector: Injector = TestBed.get(Injector); + + const componentFactoryResolver: ComponentFactoryResolver = + injector.get(ComponentFactoryResolver); + const componentFactory = + componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent); + expect(componentFactory.ngContentSelectors).toEqual(['h1', '*']); + + const nodeOne = getDOM().createTextNode('one'); + const nodeTwo = getDOM().createTextNode('two'); + const component = componentFactory.create(injector, [[nodeOne], [nodeTwo]]); + expect(component.location.nativeElement).toHaveText('(one, two)'); + }); + + modifiedInIvy( + 'FW-886: `projectableNodes` passed to a componentFactory should be in the order of' + + 'declaration. In Ivy, the ng-content slots are determined with breadth-first search.') + .it('should respect order of declaration for projectable nodes', () => { @Component({ selector: 'multiple-content-tags', - template: '(, )', + template: ` + 1 + 2 + 3 + `, }) class MultipleContentTagsComponent { } @@ -125,6 +161,7 @@ describe('projection', () => { @NgModule({ declarations: [MultipleContentTagsComponent], entryComponents: [MultipleContentTagsComponent], + imports: [CommonModule], schemas: [NO_ERRORS_SCHEMA], }) class MyModule { @@ -137,12 +174,14 @@ describe('projection', () => { injector.get(ComponentFactoryResolver); const componentFactory = componentFactoryResolver.resolveComponentFactory(MultipleContentTagsComponent); - expect(componentFactory.ngContentSelectors).toEqual(['h1', '*']); + expect(componentFactory.ngContentSelectors).toEqual(['h1', '*', 'h2']); const nodeOne = getDOM().createTextNode('one'); const nodeTwo = getDOM().createTextNode('two'); - const component = componentFactory.create(injector, [[nodeOne], [nodeTwo]]); - expect(component.location.nativeElement).toHaveText('(one, two)'); + const nodeThree = getDOM().createTextNode('three'); + const component = componentFactory.create(injector, [[nodeOne], [nodeTwo], [nodeThree]]); + component.changeDetectorRef.detectChanges(); + expect(component.location.nativeElement.textContent.trim()).toBe('1one 2two 3three'); }); it('should redistribute only direct children', () => { @@ -689,7 +728,7 @@ describe('projection', () => { @Component({selector: 'with-content', template: ''}) class WithContentCmpt { - @ViewChild('ref') directiveRef: any; + @ViewChild('ref', {static: true}) directiveRef: any; } @Component({selector: 're-project', template: ''}) diff --git a/packages/core/test/linker/query_integration_spec.ts b/packages/core/test/linker/query_integration_spec.ts index 1f68c34363..41378a6245 100644 --- a/packages/core/test/linker/query_integration_spec.ts +++ b/packages/core/test/linker/query_integration_spec.ts @@ -752,7 +752,7 @@ describe('Query API', () => { class AutoProjecting { // TODO(issue/24571): // remove '!'. - @ContentChild(TemplateRef) + @ContentChild(TemplateRef, {static: false}) content !: TemplateRef; // TODO(issue/24571): @@ -784,7 +784,7 @@ describe('Query API', () => { class AutoProjecting { // TODO(issue/24571): // remove '!'. - @ContentChild(TemplateRef) + @ContentChild(TemplateRef, {static: false}) content !: TemplateRef; // TODO(issue/24571): @@ -839,7 +839,7 @@ class NeedsContentChild implements AfterContentInit, AfterContentChecked { // TODO(issue/24571): remove '!'. _child !: TextDirective; - @ContentChild(TextDirective) + @ContentChild(TextDirective, {static: false}) set child(value) { this._child = value; this.logs.push(['setter', value ? value.text : null]); @@ -856,7 +856,7 @@ class NeedsContentChild implements AfterContentInit, AfterContentChecked { @Directive({selector: '[directive-needs-content-child]'}) class DirectiveNeedsContentChild { // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) child !: TextDirective; + @ContentChild(TextDirective, {static: false}) child !: TextDirective; } @Component({selector: 'needs-view-child', template: `
    `}) @@ -867,7 +867,7 @@ class NeedsViewChild implements AfterViewInit, AfterViewChecked { // TODO(issue/24571): remove '!'. _child !: TextDirective; - @ViewChild(TextDirective) + @ViewChild(TextDirective, {static: false}) set child(value) { this._child = value; this.logs.push(['setter', value ? value.text : null]); @@ -896,9 +896,9 @@ function createTestCmpAndDetectChanges(type: Type, template: string): Comp @Component({selector: 'needs-static-content-view-child', template: `
    `}) class NeedsStaticContentAndViewChild { // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) contentChild !: TextDirective; + @ContentChild(TextDirective, {static: true}) contentChild !: TextDirective; // TODO(issue/24571): remove '!'. - @ViewChild(TextDirective) viewChild !: TextDirective; + @ViewChild(TextDirective, {static: true}) viewChild !: TextDirective; } @Directive({selector: '[dir]'}) @@ -917,13 +917,13 @@ class NeedsQuery { @Component({selector: 'needs-four-queries', template: ''}) class NeedsFourQueries { // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) query1 !: TextDirective; + @ContentChild(TextDirective, {static: false}) query1 !: TextDirective; // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) query2 !: TextDirective; + @ContentChild(TextDirective, {static: false}) query2 !: TextDirective; // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) query3 !: TextDirective; + @ContentChild(TextDirective, {static: false}) query3 !: TextDirective; // TODO(issue/24571): remove '!'. - @ContentChild(TextDirective) query4 !: TextDirective; + @ContentChild(TextDirective, {static: false}) query4 !: TextDirective; } @Component({ @@ -1025,9 +1025,9 @@ class NeedsTpl { {selector: 'needs-named-tpl', template: '
    shadow
    '}) class NeedsNamedTpl { // TODO(issue/24571): remove '!'. - @ViewChild('tpl') viewTpl !: TemplateRef; + @ViewChild('tpl', {static: true}) viewTpl !: TemplateRef; // TODO(issue/24571): remove '!'. - @ContentChild('tpl') contentTpl !: TemplateRef; + @ContentChild('tpl', {static: true}) contentTpl !: TemplateRef; constructor(public vc: ViewContainerRef) {} } @@ -1042,9 +1042,10 @@ class NeedsContentChildrenWithRead { @Component({selector: 'needs-content-child-read', template: ''}) class NeedsContentChildWithRead { // TODO(issue/24571): remove '!'. - @ContentChild('q', {read: TextDirective}) textDirChild !: TextDirective; + @ContentChild('q', {read: TextDirective, static: false}) textDirChild !: TextDirective; // TODO(issue/24571): remove '!'. - @ContentChild('nonExisting', {read: TextDirective}) nonExistingVar !: TextDirective; + @ContentChild('nonExisting', {read: TextDirective, static: false}) + nonExistingVar !: TextDirective; } @Component({selector: 'needs-content-children-shallow', template: ''}) @@ -1059,7 +1060,7 @@ class NeedsContentChildrenShallow { }) class NeedsContentChildTemplateRef { // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) templateRef !: TemplateRef; + @ContentChild(TemplateRef, {static: true}) templateRef !: TemplateRef; } @Component({ @@ -1088,19 +1089,20 @@ class NeedsViewChildrenWithRead { }) class NeedsViewChildWithRead { // TODO(issue/24571): remove '!'. - @ViewChild('q', {read: TextDirective}) textDirChild !: TextDirective; + @ViewChild('q', {read: TextDirective, static: false}) textDirChild !: TextDirective; // TODO(issue/24571): remove '!'. - @ViewChild('nonExisting', {read: TextDirective}) nonExistingVar !: TextDirective; + @ViewChild('nonExisting', {read: TextDirective, static: false}) nonExistingVar !: TextDirective; } @Component({selector: 'needs-viewcontainer-read', template: '
    '}) class NeedsViewContainerWithRead { // TODO(issue/24571): remove '!'. - @ViewChild('q', {read: ViewContainerRef}) vc !: ViewContainerRef; + @ViewChild('q', {read: ViewContainerRef, static: false}) vc !: ViewContainerRef; // TODO(issue/24571): remove '!'. - @ViewChild('nonExisting', {read: ViewContainerRef}) nonExistingVar !: ViewContainerRef; + @ViewChild('nonExisting', {read: ViewContainerRef, static: false}) + nonExistingVar !: ViewContainerRef; // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) template !: TemplateRef; + @ContentChild(TemplateRef, {static: true}) template !: TemplateRef; createView() { this.vc.createEmbeddedView(this.template); } } @@ -1123,10 +1125,10 @@ class MyCompBroken0 { @Component({selector: 'manual-projecting', template: '
    '}) class ManualProjecting { // TODO(issue/24571): remove '!'. - @ContentChild(TemplateRef) template !: TemplateRef; + @ContentChild(TemplateRef, {static: true}) template !: TemplateRef; // TODO(issue/24571): remove '!'. - @ViewChild('vc', {read: ViewContainerRef}) + @ViewChild('vc', {read: ViewContainerRef, static: false}) vc !: ViewContainerRef; // TODO(issue/24571): remove '!'. diff --git a/packages/core/test/linker/regression_integration_spec.ts b/packages/core/test/linker/regression_integration_spec.ts index 502e16734d..85c472b201 100644 --- a/packages/core/test/linker/regression_integration_spec.ts +++ b/packages/core/test/linker/regression_integration_spec.ts @@ -362,7 +362,7 @@ function declareTests(config?: {useJit: boolean}) { @Directive({selector: 'test'}) class Test { // TODO(issue/24571): remove '!'. - @Input() @ContentChild(TemplateRef) tpl !: TemplateRef; + @Input() @ContentChild(TemplateRef, {static: true}) tpl !: TemplateRef; } @Component({ @@ -392,7 +392,7 @@ function declareTests(config?: {useJit: boolean}) { .it('should throw if @ContentChild and @Input are on the same property', () => { @Directive({selector: 'test'}) class Test { - @Input() @ContentChild(TemplateRef) tpl !: TemplateRef; + @Input() @ContentChild(TemplateRef, {static: true}) tpl !: TemplateRef; } @Component({selector: 'my-app', template: ``}) diff --git a/packages/core/test/metadata/di_spec.ts b/packages/core/test/metadata/di_spec.ts index 733328e53c..408f2ac67d 100644 --- a/packages/core/test/metadata/di_spec.ts +++ b/packages/core/test/metadata/di_spec.ts @@ -83,13 +83,13 @@ class Simple { @Component({selector: 'view-child-type-selector', template: ''}) class ViewChildTypeSelectorComponent { // TODO(issue/24571): remove '!'. - @ViewChild(Simple) child !: Simple; + @ViewChild(Simple, {static: false}) child !: Simple; } @Component({selector: 'view-child-string-selector', template: ''}) class ViewChildStringSelectorComponent { // TODO(issue/24571): remove '!'. - @ViewChild('child') child !: ElementRef; + @ViewChild('child', {static: false}) child !: ElementRef; } @Component({selector: 'view-children-type-selector', template: ''}) diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index c5e38d76d5..52629da45b 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -6,18 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {EmbeddedViewRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {withBody} from '@angular/private/testing'; -import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck, RendererType2} from '../../src/core'; +import {ChangeDetectionStrategy, DoCheck} from '../../src/core'; import {whenRendered} from '../../src/render3/component'; -import {LifecycleHooksFeature, getRenderedText, ɵɵNgOnChangesFeature, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {detectChanges, markDirty, tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; +import {LifecycleHooksFeature, getRenderedText, ɵɵdefineComponent, ɵɵgetCurrentView} from '../../src/render3/index'; +import {detectChanges, markDirty, tick, ɵɵbind, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵlistener, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; +import {Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer'; import {FLAGS, LViewFlags} from '../../src/render3/interfaces/view'; -import {ComponentFixture, containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util'; +import {containerEl, createComponent, renderComponent, requestAnimationFrame} from './render_util'; describe('change detection', () => { describe('markDirty, detectChanges, whenRendered, getRenderedText', () => { @@ -129,175 +128,6 @@ describe('change detection', () => { }); } - class MyApp { - name: string = 'Nancy'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 1, - vars: 1, - /** */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'name', ɵɵbind(ctx.name)); - } - }, - directives: () => [MyComponent] - }); - } - - it('should check OnPush components on initialization', () => { - const myApp = renderComponent(MyApp); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - }); - - it('should call doCheck even when OnPush components are not dirty', () => { - const myApp = renderComponent(MyApp); - - tick(myApp); - expect(comp.doCheckCount).toEqual(2); - - tick(myApp); - expect(comp.doCheckCount).toEqual(3); - }); - - it('should skip OnPush components in update mode when they are not dirty', () => { - const myApp = renderComponent(MyApp); - - tick(myApp); - // doCheckCount is 2, but 1 should be rendered since it has not been marked dirty. - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - tick(myApp); - // doCheckCount is 3, but 1 should be rendered since it has not been marked dirty. - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - }); - - it('should check OnPush components in update mode when inputs change', () => { - const myApp = renderComponent(MyApp); - - myApp.name = 'Bess'; - tick(myApp); - expect(comp.doCheckCount).toEqual(2); - // View should update, as changed input marks view dirty - expect(getRenderedText(myApp)).toEqual('2 - Bess'); - - myApp.name = 'George'; - tick(myApp); - // View should update, as changed input marks view dirty - expect(comp.doCheckCount).toEqual(3); - expect(getRenderedText(myApp)).toEqual('3 - George'); - - tick(myApp); - expect(comp.doCheckCount).toEqual(4); - // View should not be updated to "4", as inputs have not changed. - expect(getRenderedText(myApp)).toEqual('3 - George'); - }); - - it('should check OnPush components in update mode when component events occur', () => { - const myApp = renderComponent(MyApp); - expect(comp.doCheckCount).toEqual(1); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - const button = containerEl.querySelector('button') !; - button.click(); - requestAnimationFrame.flush(); - // No ticks should have been scheduled. - expect(comp.doCheckCount).toEqual(1); - expect(getRenderedText(myApp)).toEqual('1 - Nancy'); - - tick(myApp); - // Because the onPush comp should be dirty, it should update once CD runs - expect(comp.doCheckCount).toEqual(2); - expect(getRenderedText(myApp)).toEqual('2 - Nancy'); - }); - - it('should not check OnPush components in update mode when parent events occur', () => { - function noop() {} - - const ButtonParent = createComponent('button-parent', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - ɵɵelementStart(1, 'button', ['id', 'parent']); - { ɵɵlistener('click', () => noop()); } - ɵɵelementEnd(); - } - }, 2, 0, [MyComponent]); - - const buttonParent = renderComponent(ButtonParent); - expect(getRenderedText(buttonParent)).toEqual('1 - Nancy'); - - const button = containerEl.querySelector('button#parent') !; - (button as HTMLButtonElement).click(); - tick(buttonParent); - // The comp should still be clean. So doCheck will run, but the view should display 1. - expect(comp.doCheckCount).toEqual(2); - expect(getRenderedText(buttonParent)).toEqual('1 - Nancy'); - }); - - it('should check parent OnPush components in update mode when child events occur', () => { - let parent: ButtonParent; - - class ButtonParent implements DoCheck { - doCheckCount = 0; - ngDoCheck(): void { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: ButtonParent, - selectors: [['button-parent']], - factory: () => parent = new ButtonParent(), - consts: 2, - vars: 1, - /** {{ doCheckCount }} - */ - template: (rf: RenderFlags, ctx: ButtonParent) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - ')); - } - }, - directives: () => [MyComponent], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'button-parent'); - } - }, 1, 0, [ButtonParent]); - - const myButtonApp = renderComponent(MyButtonApp); - expect(parent !.doCheckCount).toEqual(1); - expect(comp !.doCheckCount).toEqual(1); - expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy'); - - tick(myButtonApp); - expect(parent !.doCheckCount).toEqual(2); - // parent isn't checked, so child doCheck won't run - expect(comp !.doCheckCount).toEqual(1); - expect(getRenderedText(myButtonApp)).toEqual('1 - 1 - Nancy'); - - const button = containerEl.querySelector('button'); - button !.click(); - requestAnimationFrame.flush(); - // No ticks should have been scheduled. - expect(parent !.doCheckCount).toEqual(2); - expect(comp !.doCheckCount).toEqual(1); - - tick(myButtonApp); - expect(parent !.doCheckCount).toEqual(3); - expect(comp !.doCheckCount).toEqual(2); - expect(getRenderedText(myButtonApp)).toEqual('3 - 2 - Nancy'); - }); - describe('Manual mode', () => { class ManualComponent implements DoCheck { /* @Input() */ @@ -420,12 +250,11 @@ describe('change detection', () => { }); } - const MyButtonApp = - createComponent('my-button-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'button-parent'); - } - }, 1, 0, [ButtonParent]); + const MyButtonApp = createComponent('my-button-app', function(rf: RenderFlags) { + if (rf & RenderFlags.Create) { + ɵɵelement(0, 'button-parent'); + } + }, 1, 0, [ButtonParent]); const myButtonApp = renderComponent(MyButtonApp); expect(parent !.doCheckCount).toEqual(1); @@ -462,997 +291,11 @@ describe('change detection', () => { }); }); - describe('ChangeDetectorRef', () => { - - describe('detectChanges()', () => { - let myComp: MyComp; - let dir: Dir; - - class MyComp { - doCheckCount = 0; - name = 'Nancy'; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['my-comp']], - factory: () => myComp = new MyComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ name }} */ - template: (rf: RenderFlags, ctx: MyComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.name)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - class ParentComp { - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ doCheckCount}} - - * - */ - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.doCheckCount, ' - ')); - } - }, - directives: () => [MyComp] - }); - } - - class Dir { - constructor(public cdr: ChangeDetectorRef) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir, - selectors: [['', 'dir', '']], - factory: () => dir = new Dir(ɵɵdirectiveInject(ChangeDetectorRef as any)) - }); - } - - - it('should check the component view when called by component (even when OnPush && clean)', - () => { - const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('Nancy'); - - comp.name = 'Bess'; // as this is not an Input, the component stays clean - comp.cdr.detectChanges(); - expect(getRenderedText(comp)).toEqual('Bess'); - }); - - it('should NOT call component doCheck when called by a component', () => { - const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(comp.doCheckCount).toEqual(1); - - // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you - // may see doCheck called in some cases bc of the extra CD run triggered by zone.js. - // It's important not to call doCheck to allow calls to detectChanges in that hook. - comp.cdr.detectChanges(); - expect(comp.doCheckCount).toEqual(1); - }); - - it('should NOT check the component parent when called by a child component', () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - - parentComp.doCheckCount = 100; - myComp.cdr.detectChanges(); - expect(parentComp.doCheckCount).toEqual(100); - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - }); - - it('should check component children when called by component if dirty or check-always', - () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(parentComp.doCheckCount).toEqual(1); - - myComp.name = 'Bess'; - parentComp.cdr.detectChanges(); - expect(parentComp.doCheckCount).toEqual(1); - expect(myComp.doCheckCount).toEqual(2); - // OnPush child is not dirty, so its change isn't rendered. - expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); - }); - - it('should not group detectChanges calls (call every time)', () => { - const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(myComp.doCheckCount).toEqual(1); - - parentComp.cdr.detectChanges(); - parentComp.cdr.detectChanges(); - expect(myComp.doCheckCount).toEqual(3); - }); - - it('should check component view when called by directive on component node', () => { - /** */ - const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp', ['dir', '']); - } - }, 1, 0, [MyComp, Dir]); - - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('Nancy'); - - myComp.name = 'George'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('George'); - }); - - it('should check host component when called by directive on element node', () => { - /** - * {{ name }} - *
    - */ - const MyApp = createComponent('my-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'div', ['dir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.value)); - } - }, 2, 1, [Dir]); - - const app = renderComponent(MyApp); - app.value = 'Frank'; - tick(app); - expect(getRenderedText(app)).toEqual('Frank'); - - app.value = 'Joe'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('Joe'); - }); - - it('should check the host component when called from EmbeddedViewRef', () => { - class MyApp { - showing = true; - name = 'Amelia'; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ name}} - * % if (showing) { - *
    - * % } - */ - template: function(rf: RenderFlags, ctx: MyApp) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.name)); - ɵɵcontainerRefreshStart(1); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - } - ɵɵembeddedViewEnd(); - } - ɵɵcontainerRefreshEnd(); - } - }, - directives: [Dir] - }); - } - - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('Amelia'); - - app.name = 'Emerson'; - dir !.cdr.detectChanges(); - expect(getRenderedText(app)).toEqual('Emerson'); - }); - - it('should support call in ngOnInit', () => { - class DetectChangesComp { - value = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngOnInit() { - this.value++; - this.cdr.detectChanges(); - } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetectChangesComp, - selectors: [['detect-changes-comp']], - factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: DetectChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('1'); - }); - - - ['OnInit', 'AfterContentInit', 'AfterViewInit', 'OnChanges'].forEach(hook => { - it(`should not go infinite loop when recursively called from children's ng${hook}`, () => { - class ChildComp { - // @Input - inp = ''; - - count = 0; - constructor(public parentComp: ParentComp) {} - - ngOnInit() { this.check('OnInit'); } - ngAfterContentInit() { this.check('AfterContentInit'); } - ngAfterViewInit() { this.check('AfterViewInit'); } - ngOnChanges() { this.check('OnChanges'); } - - check(h: string) { - if (h === hook) { - this.count++; - if (this.count > 1) throw new Error(`ng${hook} should be called only once!`); - this.parentComp.triggerChangeDetection(); - } - } - - static ngComponentDef = ɵɵdefineComponent({ - type: ChildComp, - selectors: [['child-comp']], - factory: () => new ChildComp(ɵɵdirectiveInject(ParentComp as any)), - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: ChildComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - }, - inputs: {inp: 'inp'}, - features: [ɵɵNgOnChangesFeature] - }); - } - - class ParentComp { - constructor(public cdr: ChangeDetectorRef) {} - - triggerChangeDetection() { this.cdr.detectChanges(); } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: ParentComp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'inp', ɵɵbind(true)); - } - }, - directives: [ChildComp] - }); - } - - expect(() => renderComponent(ParentComp)).not.toThrow(); - }); - }); - - it('should support call in ngDoCheck', () => { - class DetectChangesComp { - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { - this.doCheckCount++; - this.cdr.detectChanges(); - } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetectChangesComp, - selectors: [['detect-changes-comp']], - factory: () => new DetectChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ doCheckCount }} */ - template: (rf: RenderFlags, ctx: DetectChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.doCheckCount)); - } - } - }); - } - - const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - expect(getRenderedText(comp)).toEqual('1'); - }); - - describe('dynamic views', () => { - let structuralComp: StructuralComp|null = null; - - beforeEach(() => structuralComp = null); - - class StructuralComp { - tmp !: TemplateRef; - value = 'one'; - - constructor(public vcr: ViewContainerRef) {} - - create() { return this.vcr.createEmbeddedView(this.tmp, this); } - - static ngComponentDef = ɵɵdefineComponent({ - type: StructuralComp, - selectors: [['structural-comp']], - factory: () => structuralComp = - new StructuralComp(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'}, - consts: 1, - vars: 1, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - it('should support ViewRef.detectChanges()', () => { - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - - /** - * {{ value }} - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, FooTemplate, 1, 1, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'structural-comp'); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(1) as any; - ɵɵelementProperty(2, 'tmp', ɵɵbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(fixture.html).toEqual('one'); - - const viewRef: EmbeddedViewRef = structuralComp !.create(); - fixture.update(); - expect(fixture.html).toEqual('oneone'); - - // check embedded view update - structuralComp !.value = 'two'; - viewRef.detectChanges(); - expect(fixture.html).toEqual('onetwo'); - - // check root view update - structuralComp !.value = 'three'; - fixture.update(); - expect(fixture.html).toEqual('threethree'); - }); - - it('should support ViewRef.detectChanges() directly after creation', () => { - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Template text'); - } - } - - /** - * Template text - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'structural-comp'); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(1) as any; - ɵɵelementProperty(2, 'tmp', ɵɵbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(fixture.html).toEqual('one'); - - const viewRef: EmbeddedViewRef = structuralComp !.create(); - viewRef.detectChanges(); - expect(fixture.html).toEqual('oneTemplate text'); - }); - - }); - - }); - - describe('attach/detach', () => { - let comp: DetachedComp; - - class MyApp { - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 0, - /** */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'detached-comp'); - } - }, - directives: () => [DetachedComp] - }); - } - - class DetachedComp { - value = 'one'; - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: DetachedComp, - selectors: [['detached-comp']], - factory: () => comp = new DetachedComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: DetachedComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - it('should not check detached components', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - }); - - it('should check re-attached components', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - comp.value = 'two'; - - comp.cdr.reattach(); - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should call lifecycle hooks on detached components', () => { - const app = renderComponent(MyApp); - expect(comp.doCheckCount).toEqual(1); - - comp.cdr.detach(); - - tick(app); - expect(comp.doCheckCount).toEqual(2); - }); - - it('should check detached component when detectChanges is called', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - detectChanges(comp); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should not check detached component when markDirty is called', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - comp.cdr.detach(); - - comp.value = 'two'; - markDirty(comp); - requestAnimationFrame.flush(); - - expect(getRenderedText(app)).toEqual('one'); - }); - - it('should detach any child components when parent is detached', () => { - const app = renderComponent(MyApp); - expect(getRenderedText(app)).toEqual('one'); - - app.cdr.detach(); - - comp.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - app.cdr.reattach(); - - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - it('should detach OnPush components properly', () => { - let onPushComp: OnPushComp; - - class OnPushComp { - /** @Input() */ - // TODO(issue/24571): remove '!'. - value !: string; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushComp, - selectors: [['on-push-comp']], - factory: () => onPushComp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush, - inputs: {value: 'value'} - }); - } - - /** */ - const OnPushApp = createComponent('on-push-app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'on-push-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'value', ɵɵbind(ctx.value)); - } - }, 1, 1, [OnPushComp]); - - const app = renderComponent(OnPushApp); - app.value = 'one'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - onPushComp !.cdr.detach(); - - app.value = 'two'; - tick(app); - expect(getRenderedText(app)).toEqual('one'); - - onPushComp !.cdr.reattach(); - - tick(app); - expect(getRenderedText(app)).toEqual('two'); - }); - - }); - - describe('markForCheck()', () => { - let comp: OnPushComp; - - class OnPushComp { - value = 'one'; - - doCheckCount = 0; - - constructor(public cdr: ChangeDetectorRef) {} - - ngDoCheck() { this.doCheckCount++; } - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushComp, - selectors: [['on-push-comp']], - factory: () => comp = new OnPushComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - /** {{ value }} */ - template: (rf: RenderFlags, ctx: OnPushComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - }, - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - class OnPushParent { - value = 'one'; - - static ngComponentDef = ɵɵdefineComponent({ - type: OnPushParent, - selectors: [['on-push-parent']], - factory: () => new OnPushParent(), - consts: 2, - vars: 1, - /** - * {{ value }} - - * - */ - template: (rf: RenderFlags, ctx: OnPushParent) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'on-push-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - } - }, - directives: () => [OnPushComp], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - it('should ensure OnPush components are checked', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - - // Change detection should not have run yet, since markForCheck - // does not itself schedule change detection. - expect(fixture.hostElement.textContent).toEqual('one - one'); - - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - }); - - it('should never schedule change detection on its own', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(comp.doCheckCount).toEqual(1); - - comp.cdr.markForCheck(); - comp.cdr.markForCheck(); - requestAnimationFrame.flush(); - - expect(comp.doCheckCount).toEqual(1); - }); - - it('should ensure ancestor OnPush components are checked', () => { - const fixture = new ComponentFixture(OnPushParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - fixture.component.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('two - one'); - - }); - - it('should ensure OnPush components in embedded views are checked', () => { - class EmbeddedViewParent { - value = 'one'; - showing = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedViewParent, - selectors: [['embedded-view-parent']], - factory: () => new EmbeddedViewParent(), - consts: 2, - vars: 1, - /** - * {{ value }} - - * % if (ctx.showing) { - * - * % } - */ - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - ɵɵcontainerRefreshStart(1); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - ɵɵelement(0, 'on-push-comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - directives: () => [OnPushComp], - changeDetection: ChangeDetectionStrategy.OnPush - }); - } - - const fixture = new ComponentFixture(EmbeddedViewParent); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - one'); - - comp.cdr.markForCheck(); - // markForCheck should not trigger change detection on its own. - expect(fixture.hostElement.textContent).toEqual('one - one'); - - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - - fixture.component.value = 'two'; - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('one - two'); - - comp.cdr.markForCheck(); - tick(fixture.component); - expect(fixture.hostElement.textContent).toEqual('two - two'); - }); - - // TODO(kara): add test for dynamic views once bug fix is in - }); - - describe('checkNoChanges', () => { - let comp: NoChangesComp; - - class NoChangesComp { - value = 1; - doCheckCount = 0; - contentCheckCount = 0; - viewCheckCount = 0; - - ngDoCheck() { this.doCheckCount++; } - - ngAfterContentChecked() { this.contentCheckCount++; } - - ngAfterViewChecked() { this.viewCheckCount++; } - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: NoChangesComp, - selectors: [['no-changes-comp']], - factory: () => comp = new NoChangesComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: NoChangesComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - } - }); - } - - class AppComp { - value = 1; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: AppComp, - selectors: [['app-comp']], - factory: () => new AppComp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 2, - vars: 1, - /** - * {{ value }} - - * - */ - template: (rf: RenderFlags, ctx: AppComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵelement(1, 'no-changes-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', ctx.value, ' - ')); - } - }, - directives: () => [NoChangesComp] - }); - } - - it('should throw if bindings in current view have changed', () => { - const comp = renderComponent(NoChangesComp, {hostFeatures: [LifecycleHooksFeature]}); - - expect(() => comp.cdr.checkNoChanges()).not.toThrow(); - - comp.value = 2; - expect(() => comp.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if interpolations in current view have changed', () => { - const app = renderComponent(AppComp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - app.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if bindings in children of current view have changed', () => { - const app = renderComponent(AppComp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - comp.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should throw if bindings in embedded view have changed', () => { - class EmbeddedViewApp { - value = 1; - showing = true; - - constructor(public cdr: ChangeDetectorRef) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedViewApp, - selectors: [['embedded-view-app']], - factory: () => new EmbeddedViewApp(ɵɵdirectiveInject(ChangeDetectorRef as any)), - consts: 1, - vars: 0, - /** - * % if (showing) { - * {{ value }} - * %} - */ - template: (rf: RenderFlags, ctx: EmbeddedViewApp) => { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.showing) { - let rf0 = ɵɵembeddedViewStart(0, 1, 1); - if (rf0 & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf0 & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.value)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - }); - } - - const app = renderComponent(EmbeddedViewApp); - - expect(() => app.cdr.checkNoChanges()).not.toThrow(); - - app.value = 2; - expect(() => app.cdr.checkNoChanges()) - .toThrowError( - /ExpressionChangedAfterItHasBeenCheckedError: .+ Previous value: '1'. Current value: '2'/gi); - }); - - it('should NOT call lifecycle hooks', () => { - const app = renderComponent(AppComp); - expect(comp.doCheckCount).toEqual(1); - expect(comp.contentCheckCount).toEqual(1); - expect(comp.viewCheckCount).toEqual(1); - - comp.value = 2; - expect(() => app.cdr.checkNoChanges()).toThrow(); - expect(comp.doCheckCount).toEqual(1); - expect(comp.contentCheckCount).toEqual(1); - expect(comp.viewCheckCount).toEqual(1); - }); - - it('should NOT throw if bindings in ancestors of current view have changed', () => { - const app = renderComponent(AppComp); - - app.value = 2; - expect(() => comp.cdr.checkNoChanges()).not.toThrow(); - }); - - }); - - }); - it('should call begin and end when the renderer factory implements them', () => { const log: string[] = []; const testRendererFactory: RendererFactory3 = { - createRenderer: (hostElement: RElement | null, rendererType: RendererType2 | null): - Renderer3 => { return document; }, + createRenderer: (): Renderer3 => { return document; }, begin: () => log.push('begin'), end: () => log.push('end'), }; diff --git a/packages/core/test/render3/common_integration_spec.ts b/packages/core/test/render3/common_integration_spec.ts deleted file mode 100644 index 029c154027..0000000000 --- a/packages/core/test/render3/common_integration_spec.ts +++ /dev/null @@ -1,1094 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {NgForOfContext} from '@angular/common'; - -import {AttributeMarker, ɵɵdefineComponent, ɵɵelement, ɵɵgetCurrentView, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolationV, ɵɵlistener, ɵɵload, ɵɵnextContext, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵrestoreView} from '../../src/render3/state'; - -import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; -import {ComponentFixture, createDirective, getDirectiveOnNode} from './render_util'; - -describe('@angular/common integration', () => { - - describe('NgForOf', () => { - it('should update a loop', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵbind(item)); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - //
      - //
    • {{item}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
    • first
    • second
    '); - - // change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    • first
    • second
    '); - - // remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
    • first
    '); - - // change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
    • one
    '); - - // add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html).toEqual('
    • one
    • two
    '); - }); - - it('should support ngForOf context variables', () => { - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation3('', ctx.index, ' of ', ctx.count, ': ', item, '')); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - //
      - //
    • {{index}} of - // {{count}}: {{item}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 3, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
    • 0 of 2: first
    • 1 of 2: second
    '); - - fixture.component.items.splice(1, 0, 'middle'); - fixture.update(); - expect(fixture.html) - .toEqual('
    • 0 of 3: first
    • 1 of 3: middle
    • 2 of 3: second
    '); - }); - - it('should instantiate directives inside directives properly in an ngFor', () => { - let dirs: any[] = []; - - const Dir = createDirective('dir'); - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - factory: () => new Comp(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Comp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dir', '']); - { ɵɵtext(1, 'comp text'); } - ɵɵelementEnd(); - // testing only - dirs.push(getDirectiveOnNode(0)); - } - }, - directives: [Dir] - }); - } - - function ngForTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - } - - /** */ - class MyApp { - rows: string[] = ['first', 'second']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, ngForTemplate, 1, 0, 'comp', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.rows)); - } - }, - directives: () => [NgForOf, Comp, Dir] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
    comp text
    comp text
    '); - expect(dirs.length).toBe(2); - expect(dirs[0] instanceof Dir).toBe(true); - expect(dirs[1] instanceof Dir).toBe(true); - - fixture.component.rows.push('third'); - fixture.update(); - expect(dirs.length).toBe(3); - expect(dirs[2] instanceof Dir).toBe(true); - expect(fixture.html) - .toEqual( - '
    comp text
    comp text
    comp text
    '); - }); - - it('should retain parent view listeners when the NgFor destroy views', () => { - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item = ctx.$implicit; - ɵɵtextBinding(1, ɵɵinterpolation1('', item, '')); - } - } - - class MyApp { - private _data: number[] = [1, 2, 3]; - items: number[] = []; - - toggle() { - if (this.items.length) { - this.items = []; - } else { - this.items = this._data; - } - } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 4, - vars: 1, - // - //
      - //
    • {{index}}
    • - //
    - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', function() { return ctx.toggle(); }); - ɵɵtext(1, 'Toggle List'); - } - ɵɵelementEnd(); - ɵɵelementStart(2, 'ul'); - { - ɵɵtemplate( - 3, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(3, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - const button = fixture.hostElement.querySelector('button') !; - - expect(fixture.html).toEqual('
      '); - - // this will fill the list - fixture.component.toggle(); - fixture.update(); - expect(fixture.html) - .toEqual('
      • 1
      • 2
      • 3
      '); - - button.click(); - fixture.update(); - - expect(fixture.html).toEqual('
        '); - - button.click(); - fixture.update(); - expect(fixture.html) - .toEqual('
        • 1
        • 2
        • 3
        '); - }); - - it('should support multiple levels of embedded templates', () => { - /** - *
          - *
        • - * {{cell}} - {{ row.value }} - {{ items.length }} - * - *
        • - *
        - */ - class MyApp { - items: any[] = [{data: ['1', '2'], value: 'first'}, {data: ['3', '4'], value: 'second'}]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - { - ɵɵtemplate( - 1, liTemplate, 2, 1, 'li', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function liTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'li'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 3, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit; - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding( - 1, ɵɵinterpolation3('', cell, ' - ', row.value, ' - ', app.items.length, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
        • 1 - first - 22 - first - 2
        • 3 - second - 24 - second - 2
        '); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html) - .toEqual('
        • 1 - first - 12 - first - 1
        '); - - // Change an item - fixture.component.items[0].data[0] = 'one'; - fixture.update(); - expect(fixture.html) - .toEqual('
        • one - first - 12 - first - 1
        '); - - // Add an item - fixture.component.items[1] = {data: ['three', '4'], value: 'third'}; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        • one - first - 22 - first - 2
        • three - third - 24 - third - 2
        '); - }); - - it('should support multiple levels of embedded templates with listeners', () => { - /** - *
        - *

        - * - * {{ row.value }} - {{ name }} - *

        - *
        - */ - class MyApp { - items: any[] = [{data: ['1'], value: 'first'}]; - name = 'app'; - events: string[] = []; - - onClick(value: string, name: string) { this.events.push(value, name); } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtemplate(1, pTemplate, 3, 2, 'p', [AttributeMarker.Template, 'ngFor', 'ngForOf']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row.data)); - } - } - - function pTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - const state = ɵɵgetCurrentView(); - ɵɵelementStart(0, 'p'); - { - ɵɵelementStart(1, 'span'); - { - ɵɵlistener('click', () => { - ɵɵrestoreView(state); - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(); - app.onClick(row.value, app.name); - }); - } - ɵɵelementEnd(); - ɵɵtext(2); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext() as any; - ɵɵtextBinding(2, ɵɵinterpolation2('', row.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html).toEqual('

        first - app

        '); - - const span = fixture.hostElement.querySelector('span') as any; - span.click(); - expect(fixture.component.events).toEqual(['first', 'app']); - - fixture.component.name = 'new name'; - fixture.update(); - expect(fixture.html).toEqual('

        first - new name

        '); - - span.click(); - expect(fixture.component.events).toEqual(['first', 'app', 'first', 'new name']); - }); - - it('should support skipping contexts', () => { - /** - *
        - *
        - * - * {{ cell.value }} - {{ name }} - * - *
        - *
        - */ - class MyApp { - name = 'app'; - items: any[] = [ - [ - // row - {value: 'one', data: ['1', '2']} // cell - ], - [{value: 'two', data: ['3', '4']}] - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, divTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function divTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, innerDivTemplate, 2, 1, 'div', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(row)); - } - } - - function innerDivTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, spanTemplate, 2, 2, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(cell.data)); - } - } - - function spanTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const cell = ɵɵnextContext().$implicit as any; - const app = ɵɵnextContext(2) as any; - ɵɵtextBinding(1, ɵɵinterpolation2('', cell.value, ' - ', app.name, '')); - } - } - - const fixture = new ComponentFixture(MyApp); - - fixture.update(); - expect(fixture.html) - .toEqual( - `
        one - appone - app
        two - apptwo - app
        `); - - fixture.component.name = 'other'; - fixture.update(); - expect(fixture.html) - .toEqual( - `
        one - otherone - other
        two - othertwo - other
        `); - }); - - it('should support context for 9+ levels of embedded templates', () => { - /** - * - * - * - * - * - * - * - * - * - * - * {{ item8 }} - {{ item7.value }} - {{ item6.value }}... - * - * - * - * - * - * - * - * - * - */ - class MyApp { - value = 'App'; - items: any[] = [ - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '1', '2' // item8 - ], - value: 'h' - }], - value: 'g' - }], - value: 'f' - }], - value: 'e' - }], - value: 'd' - }], - value: 'c' - }], - value: 'b' - }], - value: 'a' - }, - { - // item0 - data: [{ - // item1 - data: [{ - // item2 - data: [{ - // item3 - data: [{ - // item4 - data: [{ - // item5 - data: [{ - // item6 - data: [{ - // item7 - data: [ - '3', '4' // item8 - ], - value: 'H' - }], - value: 'G' - }], - value: 'F' - }], - value: 'E' - }], - value: 'D' - }], - value: 'C' - }], - value: 'B' - }], - value: 'A' - } - ]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, itemTemplate0, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.items)); - } - - }, - directives: () => [NgForOf] - }); - } - - function itemTemplate0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate1, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item0 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item0.data)); - } - } - - function itemTemplate1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate2, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item1 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item1.data)); - } - } - - function itemTemplate2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate3, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item2 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item2.data)); - } - } - - function itemTemplate3(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate4, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item3 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item3.data)); - } - } - - function itemTemplate4(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate5, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item4 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item4.data)); - } - } - - function itemTemplate5(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate6, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item5 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item5.data)); - } - } - - function itemTemplate6(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate7, 2, 1, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item6 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item6.data)); - } - } - - function itemTemplate7(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { - ɵɵtemplate( - 1, itemTemplate8, 2, 10, 'span', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const item7 = ctx.$implicit as any; - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(item7.data)); - } - } - - function itemTemplate8(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - - if (rf & RenderFlags.Update) { - const value = ctx.$implicit; - const item7 = ɵɵnextContext().$implicit; - const item6 = ɵɵnextContext().$implicit; - const item5 = ɵɵnextContext().$implicit; - const item4 = ɵɵnextContext().$implicit; - const item3 = ɵɵnextContext().$implicit; - const item2 = ɵɵnextContext().$implicit; - const item1 = ɵɵnextContext().$implicit; - const item0 = ɵɵnextContext().$implicit; - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵinterpolationV([ - '', value, '.', item7.value, '.', item6.value, '.', item5.value, - '.', item4.value, '.', item3.value, '.', item2.value, '.', item1.value, - '.', item0.value, '.', myApp.value, '' - ])); - } - } - - const fixture = new ComponentFixture(MyApp); - - expect(fixture.html) - .toEqual( - '' + - '1.h.g.f.e.d.c.b.a.App' + - '2.h.g.f.e.d.c.b.a.App' + - '' + - '' + - '3.H.G.F.E.D.C.B.A.App' + - '4.H.G.F.E.D.C.B.A.App' + - ''); - }); - - }); - - describe('ngIf', () => { - it('should support sibling ngIfs', () => { - class MyApp { - showing = true; - valueOne = 'one'; - valueTwo = 'two'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 2, - /** - *
        {{ valueOne }}
        - *
        {{ valueTwo }}
        - */ - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, templateOne, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵtemplate(1, templateTwo, 2, 1, 'div', [AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - ɵɵelementProperty(1, 'ngIf', ɵɵbind(ctx.showing)); - } - - }, - directives: () => [NgIf] - }); - } - - function templateOne(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueOne)); - } - } - - function templateTwo(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const myApp = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(myApp.valueTwo)); - } - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
        one
        two
        '); - - fixture.component.valueOne = '$$one$$'; - fixture.component.valueTwo = '$$two$$'; - fixture.update(); - expect(fixture.html).toEqual('
        $$one$$
        $$two$$
        '); - }); - - it('should handle nested ngIfs with no intermediate context vars', () => { - /** - *
        - *
        - *
        - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵtemplate( - 2, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngTemplateOutlet']); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵload(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should allow usage on ng-container', () => { - class MyApp { - showing = false; - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 1, - /** - * from tpl - * - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, (rf1: RenderFlags) => { - if (rf1 & RenderFlags.Create) { - ɵɵtext(0, 'from tpl'); - } - }, 1, 0, 'ng-template', undefined, ['tpl', ''], ɵɵtemplateRefExtractor); - ɵɵelementContainerStart(2, [AttributeMarker.Bindings, 'ngTemplateOutlet']); - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.showing ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(''); - - fixture.component.showing = true; - fixture.update(); - expect(fixture.html).toEqual('from tpl'); - - fixture.component.showing = false; - fixture.update(); - expect(fixture.html).toEqual(''); - - }); - - }); -}); diff --git a/packages/core/test/render3/component_ref_spec.ts b/packages/core/test/render3/component_ref_spec.ts index 9af77d2d46..71c61c6d4f 100644 --- a/packages/core/test/render3/component_ref_spec.ts +++ b/packages/core/test/render3/component_ref_spec.ts @@ -48,7 +48,7 @@ describe('ComponentFactory', () => { consts: 0, vars: 0, template: () => undefined, - ngContentSelectors: ['a', 'b'], + ngContentSelectors: ['*', 'a', 'b'], factory: () => new TestComponent(), inputs: { in1: 'in1', diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 49aa362f8f..2f1beaa77b 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -913,7 +913,7 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]); + ɵɵprojectionDef(['*', [['span', 'title', 'toFirst']], [['span', 'title', 'toSecond']]]); ɵɵelementStart(0, 'div', ['id', 'first']); { ɵɵprojection(1, 1); } ɵɵelementEnd(); @@ -958,7 +958,7 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { ɵɵprojectionDef([ - [['span', SelectorFlags.CLASS, 'toFirst']], + '*', [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ]); ɵɵelementStart(0, 'div', ['id', 'first']); @@ -1005,7 +1005,7 @@ describe('content projection', () => { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { ɵɵprojectionDef([ - [['span', SelectorFlags.CLASS, 'toFirst']], + '*', [['span', SelectorFlags.CLASS, 'toFirst']], [['span', SelectorFlags.CLASS, 'toSecond']] ]); ɵɵelementStart(0, 'div', ['id', 'first']); @@ -1051,7 +1051,7 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]); + ɵɵprojectionDef(['*', [['span']], [['span', SelectorFlags.CLASS, 'toSecond']]]); ɵɵelementStart(0, 'div', ['id', 'first']); { ɵɵprojection(1, 1); } ɵɵelementEnd(); @@ -1095,7 +1095,7 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toFirst']]]); + ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toFirst']]]); ɵɵelementStart(0, 'div', ['id', 'first']); { ɵɵprojection(1, 1); } ɵɵelementEnd(); @@ -1140,7 +1140,7 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span', SelectorFlags.CLASS, 'toSecond']]]); + ɵɵprojectionDef(['*', [['span', SelectorFlags.CLASS, 'toSecond']]]); ɵɵelementStart(0, 'div', ['id', 'first']); { ɵɵprojection(1); } ɵɵelementEnd(); @@ -1192,7 +1192,7 @@ describe('content projection', () => { */ const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span']]]); + ɵɵprojectionDef(['*', [['span']]]); ɵɵprojection(0, 1); ɵɵelement(1, 'hr'); ɵɵprojection(2); @@ -1253,7 +1253,7 @@ describe('content projection', () => { */ const Card = createComponent('card', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['', 'card-title', '']], [['', 'card-content', '']]]); + ɵɵprojectionDef(['*', [['', 'card-title', '']], [['', 'card-content', '']]]); ɵɵprojection(0, 1); ɵɵelement(1, 'hr'); ɵɵprojection(2, 2); @@ -1306,7 +1306,7 @@ describe('content projection', () => { */ const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['div']]]); + ɵɵprojectionDef(['*', [['div']]]); ɵɵprojection(0, 1); } }, 1); diff --git a/packages/core/test/render3/debug_spec.ts b/packages/core/test/render3/debug_spec.ts index 794dfb9d56..35937c0c61 100644 --- a/packages/core/test/render3/debug_spec.ts +++ b/packages/core/test/render3/debug_spec.ts @@ -7,8 +7,8 @@ */ import {getLContext} from '../../src/render3/context_discovery'; -import {LViewDebug, toDebug} from '../../src/render3/debug'; import {RenderFlags, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵtext} from '../../src/render3/index'; +import {LViewDebug, toDebug} from '../../src/render3/instructions/lview_debug'; import {ComponentFixture} from './render_util'; diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 7ad45750f1..f56827c9ec 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectorRef, Host, InjectFlags, Injector, Optional, Renderer2, Self, ViewContainerRef} from '@angular/core'; -import {createLView, createNodeAtIndex, createTView} from '@angular/core/src/render3/instructions/shared'; +import {createLView, createTView, getOrCreateTNode} from '@angular/core/src/render3/instructions/shared'; import {RenderFlags} from '@angular/core/src/render3/interfaces/definition'; import {ɵɵdefineComponent} from '../../src/render3/definition'; @@ -16,7 +16,7 @@ import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshSt import {TNODE} from '../../src/render3/interfaces/injector'; import {TNodeType} from '../../src/render3/interfaces/node'; import {isProceduralRenderer} from '../../src/render3/interfaces/renderer'; -import {LViewFlags} from '../../src/render3/interfaces/view'; +import {LViewFlags, TVIEW} from '../../src/render3/interfaces/view'; import {enterView, leaveView} from '../../src/render3/state'; import {ViewRef} from '../../src/render3/view_ref'; @@ -605,7 +605,8 @@ describe('di', () => { null, null, {} as any, {} as any); const oldView = enterView(contentView, null); try { - const parentTNode = createNodeAtIndex(0, TNodeType.Element, null, null, null); + const parentTNode = + getOrCreateTNode(contentView[TVIEW], null, 0, TNodeType.Element, null, null); // Simulate the situation where the previous parent is not initialized. // This happens on first bootstrap because we don't init existing values // so that we have smaller HelloWorld. diff --git a/packages/core/test/render3/directive_spec.ts b/packages/core/test/render3/directive_spec.ts deleted file mode 100644 index b5bd88b269..0000000000 --- a/packages/core/test/render3/directive_spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {EventEmitter, TemplateRef, ViewContainerRef} from '@angular/core'; - -import {AttributeMarker, RenderFlags, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵlistener, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; - -import {NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture, createComponent} from './render_util'; - -describe('directive', () => { - - describe('selectors', () => { - - it('should match directives with attribute selectors on bindings', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', other: 'other'} - }); - - // TODO(issue/24571): remove '!'. - testValue !: boolean; - // TODO(issue/24571): remove '!'. - other !: boolean; - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // using 2 bindings to show example shape of attributes array - ɵɵelement(0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'test', 'other']); - } - - function updateTemplate() { ɵɵelementProperty(0, 'test', ɵɵbind(false)); } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 1, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should not accidentally set inputs from attributes extracted from bindings / outputs', - () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', prop1: 'prop1', prop2: 'prop2'} - }); - - // TODO(issue/24571): remove '!'. - prop1 !: boolean; - // TODO(issue/24571): remove '!'. - prop2 !: boolean; - // TODO(issue/24571): remove '!'. - testValue !: boolean; - - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // putting name (test) in the "usual" value position - ɵɵelement( - 0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'prop1', 'test', 'prop2']); - } - - function updateTemplate() { - ɵɵelementProperty(0, 'prop1', ɵɵbind(true)); - ɵɵelementProperty(0, 'test', ɵɵbind(false)); - ɵɵelementProperty(0, 'prop2', ɵɵbind(true)); - } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 3, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-template[directiveA]' - * }) - * export class DirectiveA { - * constructor(public templateRef: TemplateRef) {} - * } - */ - let tmplRef: any; - class DirectiveA { - constructor(public templateRef: any) { tmplRef = templateRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-template', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(TemplateRef as any)) - }); - } - - function MyComponent_ng_template_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Some content'); - } - } - class MyComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 0, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_template_Template_0, 1, 0, 'ng-template', ['directiveA', '']); - } - }, - directives: [DirectiveA] - }); - } - - new ComponentFixture(MyComponent); - expect(tmplRef instanceof TemplateRef).toBeTruthy(); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-container[directiveA]' - * }) - * export class DirectiveA { - * constructor(public vcRef: ViewContainerRef) {} - * } - */ - let vcRef: any; - class DirectiveA { - constructor(public viewContainerRef: any) { vcRef = viewContainerRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-container', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(ViewContainerRef as any)) - }); - } - - function MyComponent_ng_container_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, ['directiveA', '']); - ɵɵtext(1, 'Some content'); - ɵɵelementContainerEnd(); - } - } - class MyComponent { - visible = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 1, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_container_Template_0, 2, 0, 'ng-container', - ['directiveA', '', AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.visible)); - } - }, - directives: [DirectiveA, NgIf] - }); - } - - new ComponentFixture(MyComponent); - expect(vcRef instanceof ViewContainerRef).toBeTruthy(); - }); - - it('should match directives with attribute selectors on outputs', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - /** - * - */ - function createTemplate() { - ɵɵelementStart(0, 'span', [AttributeMarker.Bindings, 'out']); - { ɵɵlistener('out', () => {}); } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(createTemplate, () => {}, 1, 0, [Directive]); - - // "out" should not be part of reflected attributes - expect(fixture.html).toEqual(''); - expect(directiveInstance !).not.toBeUndefined(); - }); - }); - - describe('outputs', () => { - - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - it('should allow outputs of directive on ng-template', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'out']); - ɵɵlistener('out', () => { ctx.value = true; }); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - it('should allow outputs of directive on ng-container', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, [AttributeMarker.Bindings, 'out']); - { - ɵɵlistener('out', () => { ctx.value = true; }); - } - ɵɵelementContainerEnd(); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - }); -}); diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 8f47b2509a..154e974cde 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,26 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; - import {noop} from '../../../compiler/src/render3/view/util'; -import {Component as _Component} from '../../src/core'; -import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/definition'; -import {getTranslationForTemplate, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵnextContext, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n} from '../../src/render3/interfaces/i18n'; -import {AttributeMarker} from '../../src/render3/interfaces/node'; +import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n'; +import {ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view'; -import {getNativeByIndex, getTNode} from '../../src/render3/util/view_utils'; - -import {NgForOf, NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture} from './render_util'; - -const Component: typeof _Component = function(...args: any[]): any { - // In test we use @Component for documentation only so it's safe to mock out the implementation. - return () => undefined; -} as any; +import {getNativeByIndex} from '../../src/render3/util/view_utils'; +import {TemplateFixture} from './render_util'; describe('Runtime i18n', () => { describe('getTranslationForTemplate', () => { @@ -83,7 +70,6 @@ describe('Runtime i18n', () => { const index = 0; const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index); - // Check debug const debugOps = (opCodes as any).create.debug !.operations; expect(debugOps[0].__raw_opCode).toBe('simple text'); @@ -586,319 +572,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nEnd`, () => { - it('for text', () => { - const MSG_DIV = `simple text`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - expect(fixture.html).toEqual(`
        ${MSG_DIV}
        `); - }); - - it('for bindings', () => { - const MSG_DIV = `Hello �0�!`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        '); - - // But it should have created an empty text node in `viewData` - const textTNode = fixture.hostView[HEADER_OFFSET + 2] as Node; - expect(textTNode.nodeType).toEqual(Node.TEXT_NODE); - }); - - it('for elements', () => { - const MSG_DIV = `Hello �#3�world�/#3� and �#2�universe�/#2�!`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'div'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - expect(fixture.html).toEqual('
        Hello world and
        universe
        !
        '); - }); - - it('for translations without top level element', () => { - // When it's the first node - let MSG_DIV = `Hello world`; - let fixture = prepareFixture(() => { ɵɵi18n(0, MSG_DIV); }, null, 1); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is a text node - MSG_DIV = ` world`; - fixture = prepareFixture(() => { - ɵɵtext(0, 'Hello'); - ɵɵi18n(1, MSG_DIV); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - - // When the first node is an element - fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵtext(1, 'Hello'); - ɵɵelementEnd(); - ɵɵi18n(2, MSG_DIV); - }, null, 3); - - expect(fixture.html).toEqual('
        Hello
        world'); - - // When there is a node after - MSG_DIV = `Hello `; - fixture = prepareFixture(() => { - ɵɵi18n(0, MSG_DIV); - ɵɵtext(1, 'world'); - }, null, 2); - - expect(fixture.html).toEqual('Hello world'); - }); - - it('for deleted placeholders', () => { - const MSG_DIV = `Hello �#3�world�/#3�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div'); // Will be removed - ɵɵelement(3, 'span'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelementStart(4, 'div'); - { ɵɵtext(5, '!'); } - ɵɵelementEnd(); - }, null, 6); - - expect(fixture.html).toEqual('
        Hello world
        !
        '); - }); - - it('for sub-templates', () => { - // Template: `
        Content:
        beforemiddleafter
        !
        `; - const MSG_DIV = - `Content: �*2:1��#1:1�before�*2:2��#1:2�middle�/#1:2��/*2:2�after�/#1:1��/*2:1�!`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelementStart(1, 'div'); - ɵɵtemplate(2, subTemplate_2, 2, 0, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵelementEnd(); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', ɵɵbind(true)); - } - } - - function subTemplate_2(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 2); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - } - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 3, 1, 'div', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
        Content:
        beforemiddleafter
        !
        '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        -
        '); - }); - - it('for multiple ICU expressions inside html', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, null, 4); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        '); - }); - - it('for ICU expressions inside templates', () => { - const MSG_DIV = `�*2:1��#1:1�{�0:1�, plural, - =0 {no emails!} - =1 {one email} - other {�0:1� emails} - }�/#1:1��/*2:1�`; - - function subTemplate_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵi18nStart(0, MSG_DIV, 1); - ɵɵelement(1, 'span'); - ɵɵi18nEnd(); - } - if (rf & RenderFlags.Update) { - const ctx = ɵɵnextContext(); - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(0); - } - } - - class MyApp { - value0 = 0; - value1 = 'emails label'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - directives: [NgIf], - factory: () => new MyApp(), - consts: 3, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵtemplate(2, subTemplate_1, 2, 2, 'span', [AttributeMarker.Template, 'ngIf']); - ɵɵi18nEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(2, 'ngIf', true); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
        no emails!
        '); - - // Update the value - fixture.component.value0 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        3 emails
        '); - }); - - it('for ICU expressions inside ', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1); - { ɵɵi18n(2, MSG_DIV); } - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(0)); - ɵɵi18nExp(ɵɵbind('more than one')); - ɵɵi18nApply(2); - }, - 3, 2); - - expect(fixture.html).toEqual('
        no emails!
        '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        '); - }); - }); - describe(`i18nAttribute`, () => { it('for text', () => { const MSG_title = `Hello world!`; @@ -975,1140 +648,6 @@ describe('Runtime i18n', () => { }); }); - describe(`i18nExp & i18nApply`, () => { - it('for text bindings', () => { - const MSG_DIV = `Hello �0�!`; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
        Hello world!
        '); - }); - - it('for attribute bindings', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html).toEqual('
        '); - }); - - it('for attributes with no bindings', () => { - const MSG_title = `Hello world!`; - const MSG_div_attr = ['title', MSG_title]; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { ɵɵi18nApply(1); }, 2, 1); - - expect(fixture.html).toEqual('
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        '); - }); - - it('for multiple attribute bindings', () => { - const MSG_title = `Hello �0� and �1�, again �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value0: 'world', value1: 'universe'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        '); - - ctx.value0 = 'earth'; - fixture.update(); - expect(fixture.html).toEqual('
        '); - - ctx.value0 = 'earthlings'; - ctx.value1 = 'martians'; - fixture.update(); - expect(fixture.html) - .toEqual('
        '); - }); - - it('for bindings of multiple attributes', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nAttributes(1, MSG_div_attr); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value)); - ɵɵi18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        '); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html) - .toEqual('
        '); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html).toEqual('
        no emails!
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        no emails!
        '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
        one email
        '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual('
        10 emails
        '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual('
        10 emails
        '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
        no emails!
        '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - } - {�0�, select, - other {(�0�)} - }`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - expect(fixture.html) - .toEqual('
        no emails! - (0)
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual('
        no emails! - (0)
        '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html).toEqual('
        one email - (1)
        '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        10 emails - (10)
        '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        10 emails - (10)
        '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual('
        no emails! - (0)
        '); - }); - - it('for multiple ICU expressions', () => { - const MSG_DIV = `�#2�{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }�/#2��#3�{�0�, select, - other {(�0�)} - }�/#3�`; - const ctx = {value0: 0, value1: 'emails label'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18nStart(1, MSG_DIV); - ɵɵelement(2, 'span'); - ɵɵelement(3, 'span'); - ɵɵi18nEnd(); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 4, 2); - expect(fixture.html) - .toEqual( - '
        no emails!(0)
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
        no emails!(0)
        '); - - ctx.value0 = 1; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        one email(1)
        '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        10 emails(10)
        '); - - ctx.value1 = '10 emails'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        10 emails(10)
        '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html) - .toEqual( - '
        no emails!(0)
        '); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const ctx = {value0: 0, value1: 'cat'}; - - const fixture = prepareFixture( - () => { - ɵɵelementStart(0, 'div'); - ɵɵi18n(1, MSG_DIV); - ɵɵelementEnd(); - }, - () => { - ɵɵi18nExp(ɵɵbind(ctx.value0)); - ɵɵi18nExp(ɵɵbind(ctx.value1)); - ɵɵi18nApply(1); - }, - 2, 2); - - expect(fixture.html).toEqual('
        zero
        '); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
        zero
        '); - - ctx.value0 = 10; - fixture.update(); - expect(fixture.html).toEqual('
        10 cats!
        '); - - ctx.value1 = 'squirrel'; - fixture.update(); - expect(fixture.html).toEqual('
        10 animals!
        '); - - ctx.value0 = 0; - fixture.update(); - expect(fixture.html).toEqual('
        zero
        '); - }); - }); - - describe('integration', () => { - it('should support multiple i18n blocks', () => { - // Translated template: - //
        - // - // trad {{exp1}} - // - // hello - // - // - // trad - // - //
        - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_2_ATTR = ['title', `start �1� middle �0� end`]; - const MSG_DIV_2 = `�#9��/#9��#7�trad�/#7�`; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 10, - vars: 2, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'a'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵtext(3, 'hello'); - ɵɵelementStart(4, 'b'); - { - ɵɵi18nAttributes(5, MSG_DIV_2_ATTR); - ɵɵi18nStart(6, MSG_DIV_2); - { - ɵɵelement(7, 'c'); - ɵɵelement(8, 'd'); // will be removed - ɵɵelement(9, 'e'); // will be moved before `c` - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
        trad 1hellotrad
        `); - }); - - it('should support multiple sibling i18n blocks', () => { - // Translated template: - //
        - //
        Section 1
        - //
        Section 2
        - //
        Section 3
        - //
        - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - class MyApp { - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 7, - vars: 0, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'div'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'div'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'div'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual(`
        Section 1
        Section 2
        Section 3
        `); - }); - - it('should support multiple sibling i18n blocks inside of *ngFor', () => { - // Translated template: - //
          - //
        • Section 1
        • - //
        • Section 2
        • - //
        • Section 3
        • - //
        - - const MSG_DIV_1 = `Section 1`; - const MSG_DIV_2 = `Section 2`; - const MSG_DIV_3 = `Section 3`; - - function liTemplate(rf: RenderFlags, ctx: NgForOfContext) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'ul'); - ɵɵelementStart(1, 'li'); - { ɵɵi18n(2, MSG_DIV_1); } - ɵɵelementEnd(); - ɵɵelementStart(3, 'li'); - { ɵɵi18n(4, MSG_DIV_2); } - ɵɵelementEnd(); - ɵɵelementStart(5, 'li'); - { ɵɵi18n(6, MSG_DIV_3); } - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nApply(2); - ɵɵi18nApply(4); - ɵɵi18nApply(6); - } - } - - class MyApp { - items: string[] = ['1', '2', '3']; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 2, - vars: 1, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵtemplate( - 1, liTemplate, 7, 0, 'ul', [AttributeMarker.Template, 'ngFor', 'ngForOf']); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngForOf', ɵɵbind(ctx.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - `
        • Section 1
        • Section 2
        • Section 3
        • Section 1
        • Section 2
        • Section 3
        • Section 1
        • Section 2
        • Section 3
        `); - }); - - it('should support attribute translations on removed elements', () => { - // Translated template: - //
        - // trad {{exp1}} - //
        - - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 5, - vars: 5, - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b'); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual(`
        trad 1
        `); - }); - - it('should work with directives and host bindings', () => { - let directiveInstances: Directive[] = []; - - class Directive { - // @HostBinding('className') - klass = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => { - const instance = new Directive(); - directiveInstances.push(instance); - return instance; - }, - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'className', ɵɵbind(ctx.klass), null, true); - } - } - }); - } - - // Translated template: - //
        - // trad {�0�, plural, - // =0 {no emails!} - // =1 {one email} - // other {�0� emails} - // } - //
        - - const MSG_DIV_1 = `trad {�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; - - class MyApp { - exp1 = 1; - exp2 = 2; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - selectors: [['my-app']], - factory: () => new MyApp(), - consts: 6, - vars: 5, - directives: [Directive], - template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [AttributeMarker.Bindings, 'dir']); - { - ɵɵi18nAttributes(1, MSG_DIV_1_ATTR_1); - ɵɵi18nStart(2, MSG_DIV_1); - { - ɵɵelementStart(3, 'b', [AttributeMarker.Bindings, 'dir']); // Will be removed - { ɵɵi18nAttributes(4, MSG_DIV_1_ATTR_1); } - ɵɵelementEnd(); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - ɵɵelement(5, 'div', [AttributeMarker.Bindings, 'dir']); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nApply(2); - ɵɵi18nExp(ɵɵbind(ctx.exp1)); - ɵɵi18nExp(ɵɵbind(ctx.exp2)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html) - .toEqual( - `
        trad one email
        `); - - directiveInstances.forEach(instance => instance.klass = 'bar'); - fixture.component.exp1 = 2; - fixture.component.exp2 = 3; - fixture.update(); - expect(fixture.html) - .toEqual( - `
        trad 2 emails
        `); - }); - - it('should fix the links when adding/moving/removing nodes', () => { - const MSG_DIV = `�#2��/#2��#8��/#8��#4��/#4��#5��/#5�Hello World�#3��/#3��#7��/#7�`; - let fixture = prepareFixture(() => { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV); - { - ɵɵelement(2, 'div2'); - ɵɵelement(3, 'div3'); - ɵɵelement(4, 'div4'); - ɵɵelement(5, 'div5'); - ɵɵelement(6, 'div6'); - ɵɵelement(7, 'div7'); - ɵɵelement(8, 'div8'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - }, null, 9); - - expect(fixture.html) - .toEqual( - '
        Hello World
        '); - - const div0 = getTNode(0, fixture.hostView); - const div2 = getTNode(2, fixture.hostView); - const div3 = getTNode(3, fixture.hostView); - const div4 = getTNode(4, fixture.hostView); - const div5 = getTNode(5, fixture.hostView); - const div7 = getTNode(7, fixture.hostView); - const div8 = getTNode(8, fixture.hostView); - const text = getTNode(9, fixture.hostView); - expect(div0.child).toEqual(div2); - expect(div0.next).toBeNull(); - expect(div2.next).toEqual(div8); - expect(div8.next).toEqual(div4); - expect(div4.next).toEqual(div5); - expect(div5.next).toEqual(text); - expect(text.next).toEqual(div3); - expect(div3.next).toEqual(div7); - expect(div7.next).toBeNull(); - }); - - describe('projection', () => { - it('should project the translations', () => { - @Component({selector: 'child', template: '

        '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Je suis projeté depuis �#3��0��/#3��/#2�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
        - - I am projected from - {{name}} - - - - - -
        ` - // Translated to: - //
        - // - //

        - // Je suis projeté depuis {{name}} - //

        - //
        - //
        - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 8, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelementStart(2, 'child'); - { - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵelement(5, 'remove-me-1'); - } - ɵɵelementEnd(); - ɵɵelement(6, 'remove-me-2'); - } - ɵɵelementEnd(); - ɵɵelement(7, 'remove-me-3'); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(1); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

        Je suis projeté depuis Parent

        '); - //

        Parent

        - //

        Je suis projeté depuis Parent

        - }); - - it('should project a translated i18n block', () => { - @Component({selector: 'child', template: '

        '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'p'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Je suis projeté depuis �0�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
        - - - I am projected from {{name}} - - -
        ` - // Translated to: - //
        - // - // - // Je suis projeté depuis {{name}} - // - // - //
        - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'child'); - { - ɵɵelement(2, 'any'); - ɵɵelementStart(3, 'b'); - { - ɵɵi18nAttributes(4, MSG_ATTR_1); - ɵɵi18n(5, MSG_DIV_SECTION_1); - } - ɵɵelementEnd(); - ɵɵelement(6, 'any'); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(4); - ɵɵi18nExp(ɵɵbind(cmp.name)); - ɵɵi18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

        Je suis projeté depuis Parent

        '); - - // it should be able to render a new component with the same template code - const fixture2 = new ComponentFixture(Parent); - expect(fixture2.html).toEqual(fixture.html); - - // Updating the fixture should work - fixture2.component.name = 'Parent 2'; - fixture.update(); - fixture2.update(); - expect(fixture2.html) - .toEqual( - '

        Je suis projeté depuis Parent 2

        '); - - // The first fixture should not have changed - expect(fixture.html) - .toEqual( - '

        Je suis projeté depuis Parent

        '); - }); - - it('should re-project translations when multiple projections', () => { - @Component({selector: 'grand-child', template: '
        '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Bonjour�/#2� Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
        Bonjour Monde!
        - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { ɵɵelement(2, 'b'); } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
        Bonjour Monde!
        '); - //
        Bonjour
        - //
        Bonjour Monde!
        - }); - - xit('should re-project translations when removed placeholders', () => { - @Component({selector: 'grand-child', template: '
        '}) - class GrandChild { - static ngComponentDef = ɵɵdefineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'grand-child'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Bonjour Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
        Bonjour Monde!
        - }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'b'); // will be removed - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
        Bonjour Monde!
        '); - }); - - it('should project translations with selectors', () => { - @Component({ - selector: 'child', - template: ` - - ` - }) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['span']]]); - ɵɵprojection(0, 1); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`; - - @Component({ - selector: 'parent', - template: ` - - - - - ` - // Translated to: - // Contenu - }) - class Parent { - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'child'); - { - ɵɵi18nStart(1, MSG_DIV_SECTION_1); - { - ɵɵelement(2, 'span', ['title', 'keepMe']); - ɵɵelement(3, 'span', ['title', 'deleteMe']); - } - ɵɵi18nEnd(); - } - ɵɵelementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('Contenu'); - }); - }); - }); - describe('i18nPostprocess', () => { it('should handle valid cases', () => { const arr = ['�*1:1��#2:1�', '�#4:1�', '�6:1�', '�/#2:1��/*1:1�']; @@ -2241,22 +780,10 @@ describe('Runtime i18n', () => { }); it('should throw in case we have invalid string', () => { - const arr = ['�*1:1��#2:1�', '�#4:2�', '�6:4�', '�/#2:1��/*1:1�']; - const str = `[${arr.join('|')}]`; - - const cases = [ - // less placeholders than we have - [`Start: ${str}, ${str} and ${str} end.`, {}], - - // more placeholders than we have - [`Start: ${str}, ${str} and ${str}, ${str} ${str} end.`, {}], - - // not enough ICU replacements - ['My ICU #1: �I18N_EXP_ICU�, My ICU #2: �I18N_EXP_ICU�', {ICU: ['ICU_VALUE_1']}] - ]; - cases.forEach(([input, replacements, output]) => { - expect(() => ɵɵi18nPostprocess(input as string, replacements as any)).toThrowError(); - }); + expect( + () => ɵɵi18nPostprocess( + 'My ICU #1: �I18N_EXP_ICU�, My ICU #2: �I18N_EXP_ICU�', {ICU: ['ICU_VALUE_1']})) + .toThrowError(); }); }); }); diff --git a/packages/core/test/render3/inherit_definition_feature_spec.ts b/packages/core/test/render3/inherit_definition_feature_spec.ts deleted file mode 100644 index b9155bea2d..0000000000 --- a/packages/core/test/render3/inherit_definition_feature_spec.ts +++ /dev/null @@ -1,639 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {ElementRef, Inject, InjectionToken, QueryList, ɵAttributeMarker as AttributeMarker} from '../../src/core'; -import {ɵɵallocHostVars, ɵɵbind, ComponentDef, ɵɵcontentQuery, ɵɵdefineBase, ɵɵdefineComponent, ɵɵdefineDirective, DirectiveDef, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵInheritDefinitionFeature, ɵɵload, ɵɵloadContentQuery, ɵɵloadViewQuery, ɵɵNgOnChangesFeature, ɵɵProvidersFeature, ɵɵqueryRefresh, RenderFlags, ɵɵviewQuery,} from '../../src/render3/index'; - -import {ComponentFixture, createComponent, getDirectiveOnNode} from './render_util'; - -describe('InheritDefinitionFeature', () => { - it('should inherit lifecycle hooks', () => { - class SuperDirective { - ngOnInit() {} - ngOnDestroy() {} - ngAfterContentInit() {} - ngAfterContentChecked() {} - ngAfterViewInit() {} - ngAfterViewChecked() {} - ngDoCheck() {} - } - - class SubDirective extends SuperDirective { - ngAfterViewInit() {} - ngAfterViewChecked() {} - ngDoCheck() {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const finalDef = SubDirective.ngDirectiveDef as DirectiveDef; - - - expect(finalDef.onInit).toBe(SuperDirective.prototype.ngOnInit); - expect(finalDef.onDestroy).toBe(SuperDirective.prototype.ngOnDestroy); - expect(finalDef.afterContentChecked).toBe(SuperDirective.prototype.ngAfterContentChecked); - expect(finalDef.afterContentInit).toBe(SuperDirective.prototype.ngAfterContentInit); - expect(finalDef.afterViewChecked).toBe(SubDirective.prototype.ngAfterViewChecked); - expect(finalDef.afterViewInit).toBe(SubDirective.prototype.ngAfterViewInit); - expect(finalDef.doCheck).toBe(SubDirective.prototype.ngDoCheck); - }); - - it('should inherit inputs', () => { - class SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - inputs: { - superFoo: ['foo', 'declaredFoo'], - superBar: 'bar', - superBaz: 'baz', - }, - type: SuperDirective, - selectors: [['', 'superDir', '']], - factory: () => new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - inputs: { - subBaz: 'baz', - subQux: 'qux', - }, - selectors: [['', 'subDir', '']], - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const subDef = SubDirective.ngDirectiveDef as DirectiveDef; - - expect(subDef.inputs).toEqual({ - foo: 'superFoo', - bar: 'superBar', - baz: 'subBaz', - qux: 'subQux', - }); - expect(subDef.declaredInputs).toEqual({ - foo: 'declaredFoo', - bar: 'bar', - baz: 'baz', - qux: 'qux', - }); - }); - - it('should inherit outputs', () => { - class SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - outputs: { - superFoo: 'foo', - superBar: 'bar', - superBaz: 'baz', - }, - type: SuperDirective, - selectors: [['', 'superDir', '']], - factory: () => new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - outputs: { - subBaz: 'baz', - subQux: 'qux', - }, - selectors: [['', 'subDir', '']], - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const subDef = SubDirective.ngDirectiveDef as DirectiveDef; - - expect(subDef.outputs).toEqual({ - foo: 'superFoo', - bar: 'superBar', - baz: 'subBaz', - qux: 'subQux', - }); - }); - - it('should detect EMPTY inputs and outputs', () => { - class SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - inputs: { - testIn: 'testIn', - }, - outputs: { - testOut: 'testOut', - }, - type: SuperDirective, - selectors: [['', 'superDir', '']], - factory: () => new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const subDef = SubDirective.ngDirectiveDef as DirectiveDef; - - expect(subDef.inputs).toEqual({ - testIn: 'testIn', - }); - expect(subDef.outputs).toEqual({ - testOut: 'testOut', - }); - }); - - it('should inherit inputs from ngBaseDefs along the way', () => { - - class Class5 { - input5 = 'data, so data'; - - static ngBaseDef = ɵɵdefineBase({ - inputs: { - input5: 'input5', - }, - }); - } - - class Class4 extends Class5 { - input4 = 'hehe'; - - static ngDirectiveDef = ɵɵdefineDirective({ - inputs: { - input4: 'input4', - }, - type: Class4, - selectors: [['', 'superDir', '']], - factory: () => new Class4(), - features: [ɵɵInheritDefinitionFeature], - }); - } - - class Class3 extends Class4 {} - - class Class2 extends Class3 { - input3 = 'wee'; - - static ngBaseDef = ɵɵdefineBase({ - inputs: { - input3: ['alias3', 'input3'], - } - }) as any; - } - - class Class1 extends Class2 { - input1 = 'test'; - input2 = 'whatever'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Class1, - inputs: { - input1: 'input1', - input2: 'input2', - }, - selectors: [['', 'subDir', '']], - factory: () => new Class1(), - features: [ɵɵInheritDefinitionFeature], - }); - } - - const subDef = Class1.ngDirectiveDef as DirectiveDef; - - expect(subDef.inputs).toEqual({ - input1: 'input1', - input2: 'input2', - alias3: 'input3', - input4: 'input4', - input5: 'input5', - }); - expect(subDef.declaredInputs).toEqual({ - input1: 'input1', - input2: 'input2', - alias3: 'input3', - input4: 'input4', - input5: 'input5', - }); - }); - - it('should inherit outputs from ngBaseDefs along the way', () => { - - class Class5 { - output5 = 'data, so data'; - - static ngBaseDef = ɵɵdefineBase({ - outputs: { - output5: 'alias5', - }, - }); - } - - class Class4 extends Class5 { - output4 = 'hehe'; - - static ngDirectiveDef = ɵɵdefineDirective({ - outputs: { - output4: 'alias4', - }, - type: Class4, - selectors: [['', 'superDir', '']], - factory: () => new Class4(), - features: [ɵɵInheritDefinitionFeature], - }); - } - - class Class3 extends Class4 {} - - class Class2 extends Class3 { - output3 = 'wee'; - - static ngBaseDef = ɵɵdefineBase({ - outputs: { - output3: 'alias3', - } - }) as any; - } - - class Class1 extends Class2 { - output1 = 'test'; - output2 = 'whatever'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Class1, - outputs: { - output1: 'alias1', - output2: 'alias2', - }, - selectors: [['', 'subDir', '']], - factory: () => new Class1(), - features: [ɵɵInheritDefinitionFeature], - }); - } - - const subDef = Class1.ngDirectiveDef as DirectiveDef; - - expect(subDef.outputs).toEqual({ - alias1: 'output1', - alias2: 'output2', - alias3: 'output3', - alias4: 'output4', - alias5: 'output5', - }); - }); - - it('should compose hostBindings', () => { - let subDir !: SubDirective; - - class SuperDirective { - id = 'my-id'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SuperDirective, - selectors: [['', 'superDir', '']], - hostBindings: (rf: RenderFlags, ctx: SuperDirective, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'id', ɵɵbind(ctx.id)); - } - }, - factory: () => new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - title = 'my-title'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - hostBindings: (rf: RenderFlags, ctx: SubDirective, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'title', ɵɵbind(ctx.title)); - } - }, - factory: () => subDir = new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['subDir', '']); - } - }, 1, 0, [SubDirective]); - - const fixture = new ComponentFixture(App); - const divEl = fixture.hostElement.querySelector('div') as HTMLElement; - - expect(divEl.id).toEqual('my-id'); - expect(divEl.title).toEqual('my-title'); - - subDir.title = 'new-title'; - fixture.update(); - expect(divEl.id).toEqual('my-id'); - expect(divEl.title).toEqual('new-title'); - - subDir.id = 'new-id'; - fixture.update(); - expect(divEl.id).toEqual('new-id'); - expect(divEl.title).toEqual('new-title'); - }); - - describe('view query', () => { - - const SomeComp = createComponent('some-comp', (rf: RenderFlags, ctx: any) => {}); - - /* - * class SuperComponent { - * @ViewChildren('super') superQuery; - * } - */ - class SuperComponent { - superQuery?: QueryList; - static ngComponentDef = ɵɵdefineComponent({ - type: SuperComponent, - template: () => {}, - consts: 0, - vars: 0, - selectors: [['super-comp']], - viewQuery: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['super'], false, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.superQuery = tmp as QueryList); - } - }, - factory: () => new SuperComponent(), - }); - } - - /** - *
        - *
        - * - * class SubComponent extends SuperComponent { - * @ViewChildren('sub') subQuery; - * } - */ - class SubComponent extends SuperComponent { - subQuery?: QueryList; - static ngComponentDef = ɵɵdefineComponent({ - type: SubComponent, - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['id', 'sub'], ['sub', '']); - ɵɵelement(2, 'div', ['id', 'super'], ['super', '']); - ɵɵelement(4, 'some-comp'); - } - }, - consts: 5, - vars: 0, - selectors: [['sub-comp']], - viewQuery: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['sub'], false, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.subQuery = tmp as QueryList); - } - }, - factory: () => new SubComponent(), - features: [ɵɵInheritDefinitionFeature], - directives: [SomeComp] - }); - } - - - it('should compose viewQuery (basic mechanics check)', () => { - const log: Array<[string, RenderFlags, any]> = []; - - class SuperComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SuperComponent, - template: () => {}, - consts: 0, - vars: 0, - selectors: [['', 'superDir', '']], - viewQuery: (rf: RenderFlags, ctx: T) => { - log.push(['super', rf, ctx]); - }, - factory: () => new SuperComponent(), - }); - } - - class SubComponent extends SuperComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SubComponent, - template: () => {}, - consts: 0, - vars: 0, - selectors: [['', 'subDir', '']], - viewQuery: (rf: RenderFlags, ctx: SubComponent) => { - log.push(['sub', rf, ctx]); - }, - factory: () => new SubComponent(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const subDef = SubComponent.ngComponentDef as ComponentDef; - - const context = {foo: 'bar'}; - - subDef.viewQuery !(RenderFlags.Create, context); - - expect(log).toEqual( - [['super', RenderFlags.Create, context], ['sub', RenderFlags.Create, context]]); - }); - - - - it('should compose viewQuery (query logic check)', () => { - const fixture = new ComponentFixture(SubComponent); - - const check = (key: string): void => { - const qList = (fixture.component as any)[`${key}Query`] as QueryList; - expect(qList.length).toBe(1); - expect(qList.first.nativeElement).toEqual(fixture.hostElement.querySelector(`#${key}`)); - expect(qList.first.nativeElement.id).toEqual(key); - }; - - check('sub'); - check('super'); - }); - - it('should work with multiple viewQuery comps', () => { - let subCompOne !: SubComponent; - let subCompTwo !: SubComponent; - - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'sub-comp'); - ɵɵelement(1, 'sub-comp'); - } - subCompOne = getDirectiveOnNode(0); - subCompTwo = getDirectiveOnNode(1); - }, 2, 0, [SubComponent, SuperComponent]); - - const fixture = new ComponentFixture(App); - - const check = (comp: SubComponent): void => { - const qList = comp.subQuery as QueryList; - expect(qList.length).toBe(1); - expect(qList.first.nativeElement).toEqual(fixture.hostElement.querySelector('#sub')); - expect(qList.first.nativeElement.id).toEqual('sub'); - }; - - check(subCompOne); - check(subCompTwo); - }); - - }); - - - it('should compose contentQueries (basic mechanics check)', () => { - const log: string[] = []; - - class SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: SuperDirective, - selectors: [['', 'superDir', '']], - contentQueries: () => { log.push('super'); }, - factory: () => new SuperDirective(), - }); - } - - class SubDirective extends SuperDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - contentQueries: () => { log.push('sub'); }, - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - }); - } - - const subDef = SubDirective.ngDirectiveDef as DirectiveDef; - - subDef.contentQueries !(RenderFlags.Create, {}, 0); - - expect(log).toEqual(['super', 'sub']); - }); - - it('should compose contentQueries (verify query sets)', () => { - let dirInstance: SubDirective; - class SuperDirective { - // @ContentChildren('foo') - foos !: QueryList; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SuperDirective, - selectors: [['', 'super-dir', '']], - factory: () => new SuperDirective(), - contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵcontentQuery(dirIndex, ['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadContentQuery()) && (ctx.foos = tmp); - } - } - }); - } - - class SubDirective extends SuperDirective { - // @ContentChildren('bar') - bars !: QueryList; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'sub-dir', '']], - factory: () => dirInstance = new SubDirective(), - contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵcontentQuery(dirIndex, ['bar'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadContentQuery()) && (ctx.bars = tmp); - } - }, - features: [ɵɵInheritDefinitionFeature] - }); - } - - /** - *
        - * - * - *
        - */ - const AppComponent = createComponent('app-component', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [AttributeMarker.Bindings, 'sub-dir']); - { - ɵɵelement(1, 'span', null, ['foo', '']); - ɵɵelement(3, 'span', null, ['bar', '']); - } - ɵɵelementEnd(); - } - }, 5, 0, [SubDirective]); - - const fixture = new ComponentFixture(AppComponent); - expect(dirInstance !.foos.length).toBe(1); - expect(dirInstance !.bars.length).toBe(1); - }); - - it('should throw if inheriting a component from a directive', () => { - class SuperComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SuperComponent, - template: () => {}, - selectors: [['', 'superDir', '']], - consts: 0, - vars: 0, - factory: () => new SuperComponent() - }); - } - - expect(() => { - class SubDirective extends SuperComponent{static ngDirectiveDef = ɵɵdefineDirective({ - type: SubDirective, - selectors: [['', 'subDir', '']], - factory: () => new SubDirective(), - features: [ɵɵInheritDefinitionFeature] - });} - }).toThrowError('Directives cannot inherit Components'); - }); - -}); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 55090eb9f9..0674229a90 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -6,1080 +6,42 @@ * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; - import {RendererType2} from '../../src/render/api'; import {getLContext} from '../../src/render3/context_discovery'; -import {AttributeMarker, ɵɵclassMap, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵstyleMap, ɵɵtemplateRefExtractor} from '../../src/render3/index'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵclassProp, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementAttribute, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementHostAttrs, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵinterpolation2, ɵɵinterpolation3, ɵɵinterpolation4, ɵɵinterpolation5, ɵɵinterpolation6, ɵɵinterpolation7, ɵɵinterpolation8, ɵɵinterpolationV, ɵɵprojection, ɵɵprojectionDef, ɵɵreference, ɵɵselect, ɵɵstyleProp, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; +import {AttributeMarker, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; +import {ɵɵallocHostVars, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementAttribute, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵselect, ɵɵstyling, ɵɵstylingApply, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {StylingIndex} from '../../src/render3/interfaces/styling'; import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view'; -import {ɵɵdisableBindings, ɵɵenableBindings} from '../../src/render3/state'; import {ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {NgIf} from './common_with_def'; -import {ComponentFixture, MockRendererFactory, TemplateFixture, createComponent, renderToHtml} from './render_util'; +import {ComponentFixture, MockRendererFactory, renderToHtml} from './render_util'; describe('render3 integration test', () => { describe('render', () => { - - it('should render basic template', () => { - expect(renderToHtml(Template, {}, 2)).toEqual('Greetings'); - - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span', ['title', 'Hello']); - { ɵɵtext(1, 'Greetings'); } - ɵɵelementEnd(); - } - } - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 1, - tNode: 3, // 1 for div, 1 for text, 1 for host element - tView: 2, // 1 for root view, 1 for template - rendererCreateElement: 1, - }); - }); - - it('should render and update basic "Hello, World" template', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'h1'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵinterpolation1('Hello, ', ctx.name, '!')); - } - }, 2, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'World'; - fixture.update(); - expect(fixture.html).toEqual('

        Hello, World!

        '); - - fixture.component.name = 'New World'; - fixture.update(); - expect(fixture.html).toEqual('

        Hello, New World!

        '); - }); - }); - - describe('text bindings', () => { - it('should render "undefined" as "" when used with `bind()`', () => { - function Template(rf: RenderFlags, name: string) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(name)); - } - } - - expect(renderToHtml(Template, 'benoit', 1, 1)).toEqual('benoit'); - expect(renderToHtml(Template, undefined, 1, 1)).toEqual(''); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 2, - }); - }); - - it('should render "null" as "" when used with `bind()`', () => { - function Template(rf: RenderFlags, name: string) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(name)); - } - } - - expect(renderToHtml(Template, 'benoit', 1, 1)).toEqual('benoit'); - expect(renderToHtml(Template, null, 1, 1)).toEqual(''); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 2, - }); - }); - - it('should support creation-time values in text nodes', () => { - function Template(rf: RenderFlags, value: string) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - ɵɵtextBinding(0, value); - } - } - expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); - expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once'); - expect(ngDevMode).toHaveProperties({ - firstTemplatePass: 0, - tNode: 2, - tView: 2, // 1 for root view, 1 for template - rendererSetText: 1, - }); - }); - - }); - - - describe('ngNonBindable handling', () => { - it('should keep local ref for host element', () => { - /** - * - * Hello {{ name }}! - * - * {{ myRef.id }} - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b', ['id', 'my-id'], ['myRef', '']); - ɵɵdisableBindings(); - ɵɵelementStart(2, 'i'); - ɵɵtext(3, 'Hello {{ name }}!'); - ɵɵelementEnd(); - ɵɵenableBindings(); - ɵɵelementEnd(); - ɵɵtext(4); - } - if (rf & RenderFlags.Update) { - const ref = ɵɵreference(1) as any; - ɵɵtextBinding(4, ɵɵinterpolation1(' ', ref.id, ' ')); - } - }, 5, 1); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}! my-id '); - }); - - it('should invoke directives for host element', () => { - let directiveInvoked: boolean = false; - - class TestDirective { - ngOnInit() { directiveInvoked = true; } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: TestDirective, - selectors: [['', 'directive', '']], - factory: () => new TestDirective() - }); - } - - /** - * - * Hello {{ name }}! - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b', ['directive', '']); - ɵɵdisableBindings(); - ɵɵelementStart(1, 'i'); - ɵɵtext(2, 'Hello {{ name }}!'); - ɵɵelementEnd(); - ɵɵenableBindings(); - ɵɵelementEnd(); - } - }, 3, 0, [TestDirective]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}!'); - expect(directiveInvoked).toEqual(true); - }); - - it('should not invoke directives for nested elements', () => { - let directiveInvoked: boolean = false; - - class TestDirective { - ngOnInit() { directiveInvoked = true; } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: TestDirective, - selectors: [['', 'directive', '']], - factory: () => new TestDirective() - }); - } - - /** - * - * Hello {{ name }}! - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b'); - ɵɵdisableBindings(); - ɵɵelementStart(1, 'i', ['directive', '']); - ɵɵtext(2, 'Hello {{ name }}!'); - ɵɵelementEnd(); - ɵɵenableBindings(); - ɵɵelementEnd(); - } - }, 3, 0, [TestDirective]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('Hello {{ name }}!'); - expect(directiveInvoked).toEqual(false); - }); - }); - - describe('Siblings update', () => { - it('should handle a flat list of static/bound text nodes', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Hello '); - ɵɵtext(1); - ɵɵtext(2, '!'); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.name)); - } - }, 3, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'monde'; - fixture.update(); - expect(fixture.html).toEqual('Hello monde!'); - }); - - it('should handle a list of static/bound text nodes as element children', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b'); - { - ɵɵtext(1, 'Hello '); - ɵɵtext(2); - ɵɵtext(3, '!'); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(2, ɵɵbind(ctx.name)); - } - }, 4, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'mundo'; - fixture.update(); - expect(fixture.html).toEqual('Hello mundo!'); - }); - - it('should render/update text node as a child of a deep list of elements', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b'); - { - ɵɵelementStart(1, 'b'); - { - ɵɵelementStart(2, 'b'); - { - ɵɵelementStart(3, 'b'); - { ɵɵtext(4); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(4, ɵɵinterpolation1('Hello ', ctx.name, '!')); - } - }, 5, 1); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'world'; - fixture.update(); - expect(fixture.html).toEqual('Hello world!'); - - fixture.component.name = 'mundo'; - fixture.update(); - expect(fixture.html).toEqual('Hello mundo!'); - }); - - it('should update 2 sibling elements', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'b'); - { - ɵɵelement(1, 'span'); - ɵɵelementStart(2, 'span', ['class', 'foo']); - {} - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(2, 'id', ɵɵbind(ctx.id)); - } - }, 3, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'foo'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'bar'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should handle sibling text node after element with child text node', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'p'); - { ɵɵtext(1, 'hello'); } - ɵɵelementEnd(); - ɵɵtext(2, 'world'); - } - }, 3); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

        hello

        world'); - }); - }); - - describe('basic components', () => { - - class TodoComponent { - value = ' one'; - - static ngComponentDef = ɵɵdefineComponent({ - type: TodoComponent, - selectors: [['todo']], - consts: 3, - vars: 1, - template: function TodoTemplate(rf: RenderFlags, ctx: any) { + describe('text bindings', () => { + it('should support creation-time values in text nodes', () => { + function Template(rf: RenderFlags, value: string) { if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'p'); - { - ɵɵtext(1, 'Todo'); - ɵɵtext(2); - } - ɵɵelementEnd(); + ɵɵtext(0); + ɵɵtextBinding(0, value); } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(2, ɵɵbind(ctx.value)); - } - }, - factory: () => new TodoComponent + } + expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); + expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once'); + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 0, + tNode: 2, + tView: 2, // 1 for root view, 1 for template + rendererSetText: 1, + }); }); - } - - const defs = [TodoComponent]; - - it('should support a basic component template', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'todo'); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

        Todo one

        '); }); - - it('should support a component template with sibling', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'todo'); - ɵɵtext(1, 'two'); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

        Todo one

        two'); - }); - - it('should support a component template with component sibling', () => { - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'todo'); - ɵɵelement(1, 'todo'); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

        Todo one

        Todo one

        '); - }); - - it('should support a component with binding on host element', () => { - let cmptInstance: TodoComponentHostBinding|null; - - class TodoComponentHostBinding { - title = 'one'; - static ngComponentDef = ɵɵdefineComponent({ - type: TodoComponentHostBinding, - selectors: [['todo']], - consts: 1, - vars: 1, - template: function TodoComponentHostBindingTemplate( - rf: RenderFlags, ctx: TodoComponentHostBinding) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.title)); - } - }, - factory: () => cmptInstance = new TodoComponentHostBinding, - hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - // host bindings - ɵɵelementProperty(elementIndex, 'title', ɵɵbind(ctx.title)); - } - } - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'todo'); - } - }, 1, 0, [TodoComponentHostBinding]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('one'); - - cmptInstance !.title = 'two'; - fixture.update(); - expect(fixture.html).toEqual('two'); - }); - - it('should support root component with host attribute', () => { - class HostAttributeComp { - static ngComponentDef = ɵɵdefineComponent({ - type: HostAttributeComp, - selectors: [['host-attr-comp']], - factory: () => new HostAttributeComp(), - consts: 0, - vars: 0, - hostBindings: function(rf, ctx, elIndex) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs(['role', 'button']); - } - }, - template: (rf: RenderFlags, ctx: HostAttributeComp) => {}, - }); - } - - const fixture = new ComponentFixture(HostAttributeComp); - expect(fixture.hostElement.getAttribute('role')).toEqual('button'); - }); - - it('should support component with bindings in template', () => { - /**

        {{ name }}

        */ - class MyComp { - name = 'Bess'; - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['comp']], - consts: 2, - vars: 1, - template: function MyCompTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'p'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(ctx.name)); - } - }, - factory: () => new MyComp - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, [MyComp]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('

        Bess

        '); - }); - - it('should support a component with sub-views', () => { - /** - * % if (condition) { - *
        text
        - * % } - */ - class MyComp { - // TODO(issue/24571): remove '!'. - condition !: boolean; - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['comp']], - consts: 1, - vars: 0, - template: function MyCompTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 2, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1, 'text'); } - ɵɵelementEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, - factory: () => new MyComp, - inputs: {condition: 'condition'} - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'condition', ɵɵbind(ctx.condition)); - } - }, 1, 1, [MyComp]); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html).toEqual('
        text
        '); - - fixture.component.condition = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - }); - - describe('ng-container', () => { - - it('should insert as a child of a regular element', () => { - /** - *
        before|Greetings|after
        - */ - function Template() { - ɵɵelementStart(0, 'div'); - { - ɵɵtext(1, 'before|'); - ɵɵelementContainerStart(2); - { - ɵɵtext(3, 'Greetings'); - ɵɵelement(4, 'span'); - } - ɵɵelementContainerEnd(); - ɵɵtext(5, '|after'); - } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 6); - expect(fixture.html).toEqual('
        before|Greetings|after
        '); - }); - - it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { - /** - * {% if (value) { %} - *
        - * content - *
        - * {% } %} - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - if (ctx.value) { - let rf1 = ɵɵembeddedViewStart(0, 3, 0); - { - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1); - { ɵɵtext(2, 'content'); } - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - } - } - ɵɵembeddedViewEnd(); - } - ɵɵcontainerRefreshEnd(); - } - }, 1); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('
        content
        '); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should add and remove DOM nodes when ng-container is a child of an embedded view (JS block)', - () => { - /** - * {% if (value) { %} - * content - * {% } %} - */ - const TestCmpt = - createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - if (ctx.value) { - let rf1 = ɵɵembeddedViewStart(0, 2, 0); - { - if (rf1 & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { ɵɵtext(1, 'content'); } - ɵɵelementContainerEnd(); - } - } - ɵɵembeddedViewEnd(); - } - ɵɵcontainerRefreshEnd(); - } - }, 1); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('content'); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should add and remove DOM nodes when ng-container is a child of an embedded view (ViewContainerRef)', - () => { - - function ngIfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { ɵɵtext(1, 'content'); } - ɵɵelementContainerEnd(); - } - } - - /** - * content - */ - // equivalent to: - /** - * - * - * content - * - * - */ - const TestCmpt = - createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, ngIfTemplate, 2, 0, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.value)); - } - }, 1, 1, [NgIf]); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - fixture.component.value = true; - fixture.update(); - expect(fixture.html).toEqual('content'); - - fixture.component.value = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - // https://stackblitz.com/edit/angular-tfhcz1?file=src%2Fapp%2Fapp.component.ts - it('should add and remove DOM nodes when ng-container is a child of a delayed embedded view', - () => { - - class TestDirective { - constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} - - createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: TestDirective, - selectors: [['', 'testDirective', '']], - factory: () => testDirective = new TestDirective( - ɵɵdirectiveInject(TemplateRef as any), - ɵɵdirectiveInject(ViewContainerRef as any)), - }); - } - - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { ɵɵtext(1, 'content'); } - ɵɵelementContainerEnd(); - } - } - - let testDirective: TestDirective; - - - ` - - content - - `; - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 2, 0, 'ng-template', - [AttributeMarker.Bindings, 'testDirective']); - } - }, 1, 0, [TestDirective]); - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('content'); - - testDirective !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should render at the component view root', () => { - /** - * component template - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { ɵɵtext(1, 'component template'); } - ɵɵelementContainerEnd(); - } - }, 2); - - function App() { ɵɵelement(0, 'test-cmpt'); } - - const fixture = new TemplateFixture(App, () => {}, 1, 0, [TestCmpt]); - expect(fixture.html).toEqual('component template'); - }); - - it('should render inside another ng-container', () => { - /** - * - * - * - * content - * - * - * - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { - ɵɵelementContainerStart(1); - { - ɵɵelementContainerStart(2); - { ɵɵtext(3, 'content'); } - ɵɵelementContainerEnd(); - } - ɵɵelementContainerEnd(); - } - ɵɵelementContainerEnd(); - } - }, 4); - - function App() { ɵɵelement(0, 'test-cmpt'); } - - const fixture = new TemplateFixture(App, () => {}, 1, 0, [TestCmpt]); - expect(fixture.html).toEqual('content'); - }); - - it('should render inside another ng-container at the root of a delayed view', () => { - let testDirective: TestDirective; - - class TestDirective { - constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} - - createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: TestDirective, - selectors: [['', 'testDirective', '']], - factory: () => testDirective = new TestDirective( - ɵɵdirectiveInject(TemplateRef as any), - ɵɵdirectiveInject(ViewContainerRef as any)), - }); - } - - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { - ɵɵelementContainerStart(1); - { - ɵɵelementContainerStart(2); - { ɵɵtext(3, 'content'); } - ɵɵelementContainerEnd(); - } - ɵɵelementContainerEnd(); - } - ɵɵelementContainerEnd(); - } - } - - /** - * - * - * - * - * content - * - * - * - * - */ - const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 4, 0, 'ng-template', - [AttributeMarker.Bindings, 'testDirective']); - } - }, 1, 0, [TestDirective]); - - function App() { ɵɵelement(0, 'test-cmpt'); } - - const fixture = new ComponentFixture(TestCmpt); - expect(fixture.html).toEqual(''); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('content'); - - testDirective !.createAndInsert(); - fixture.update(); - expect(fixture.html).toEqual('contentcontent'); - - testDirective !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support directives and inject ElementRef', () => { - - class Directive { - constructor(public elRef: ElementRef) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directive = new Directive(ɵɵdirectiveInject(ElementRef)), - }); - } - - let directive: Directive; - - /** - *
        - */ - function Template() { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1, [AttributeMarker.Bindings, 'dir']); - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 2, 0, [Directive]); - expect(fixture.html).toEqual('
        '); - expect(directive !.elRef.nativeElement.nodeType).toBe(Node.COMMENT_NODE); - }); - - it('should support ViewContainerRef when ng-container is at the root of a view', () => { - - function ContentTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Content'); - } - } - - class Directive { - contentTpl: TemplateRef<{}>|null = null; - - constructor(private _vcRef: ViewContainerRef) {} - - insertView() { this._vcRef.createEmbeddedView(this.contentTpl as TemplateRef<{}>); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directive = new Directive(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {contentTpl: 'contentTpl'}, - }); - } - - let directive: Directive; - - /** - * - * Content - * - */ - const App = createComponent('app', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, [AttributeMarker.Bindings, 'dir']); - ɵɵtemplate( - 1, ContentTemplate, 1, 0, 'ng-template', null, ['content', ''], - ɵɵtemplateRefExtractor); - ɵɵelementContainerEnd(); - } - if (rf & RenderFlags.Update) { - const content = ɵɵreference(2) as any; - ɵɵelementProperty(0, 'contentTpl', ɵɵbind(content)); - } - }, 3, 1, [Directive]); - - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(''); - - directive !.insertView(); - fixture.update(); - expect(fixture.html).toEqual('Content'); - - directive !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support ViewContainerRef on inside ', () => { - function ContentTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Content'); - } - } - - class Directive { - constructor(private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef) {} - - insertView() { this._vcRef.createEmbeddedView(this._tplRef); } - - clear() { this._vcRef.clear(); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => directive = new Directive( - ɵɵdirectiveInject(TemplateRef as any), - ɵɵdirectiveInject(ViewContainerRef as any)), - }); - } - - let directive: Directive; - - /** - * - * Content - * - */ - const App = createComponent('app', function(rf: RenderFlags) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - ɵɵtemplate( - 1, ContentTemplate, 1, 0, 'ng-template', [AttributeMarker.Bindings, 'dir'], [], - ɵɵtemplateRefExtractor); - ɵɵelementContainerEnd(); - } - }, 2, 0, [Directive]); - - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(''); - - directive !.insertView(); - fixture.update(); - expect(fixture.html).toEqual('Content'); - - directive !.clear(); - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should not set any attributes', () => { - /** - *
        - */ - function Template() { - ɵɵelementStart(0, 'div'); - { - ɵɵelementContainerStart(1, ['id', 'foo']); - ɵɵelementContainerEnd(); - } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(Template, () => {}, 2); - expect(fixture.html).toEqual('
        '); - }); - }); describe('tree', () => { @@ -1232,971 +194,6 @@ describe('render3 integration test', () => { }); - describe('element bindings', () => { - - describe('elementAttribute', () => { - it('should support attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(0, 'title', ɵɵbind(ctx.title)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = 'Hello'; - fixture.update(); - // initial binding - expect(fixture.html).toEqual(''); - - // update binding - fixture.component.title = 'Hi!'; - fixture.update(); - expect(fixture.html).toEqual(''); - - // remove attribute - fixture.component.title = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should stringify values used attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(0, 'title', ɵɵbind(ctx.title)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = NaN; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.title = {toString: () => 'Custom toString'}; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should update bindings', () => { - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'b'); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(0, 'a', ɵɵinterpolationV(c)); - ɵɵelementAttribute(0, 'a0', ɵɵbind(c[1])); - ɵɵelementAttribute(0, 'a1', ɵɵinterpolation1(c[0], c[1], c[16])); - ɵɵelementAttribute(0, 'a2', ɵɵinterpolation2(c[0], c[1], c[2], c[3], c[16])); - ɵɵelementAttribute( - 0, 'a3', ɵɵinterpolation3(c[0], c[1], c[2], c[3], c[4], c[5], c[16])); - ɵɵelementAttribute( - 0, 'a4', ɵɵinterpolation4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16])); - ɵɵelementAttribute( - 0, 'a5', ɵɵinterpolation5( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16])); - ɵɵelementAttribute( - 0, 'a6', ɵɵinterpolation6( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[16])); - ɵɵelementAttribute( - 0, 'a7', ɵɵinterpolation7( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[12], c[13], c[16])); - ɵɵelementAttribute( - 0, 'a8', ɵɵinterpolation8( - c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], - c[11], c[12], c[13], c[14], c[15], c[16])); - } - } - let args = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - args = args.reverse(); - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - args = args.reverse(); - expect(renderToHtml(Template, args, 1, 54)) - .toEqual( - ''); - }); - - it('should not update DOM if context has not changed', () => { - const ctx: {title: string | null} = {title: 'Hello'}; - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵcontainer(1); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(0, 'title', ɵɵbind(ctx.title)); - ɵɵcontainerRefreshStart(1); - { - if (true) { - let rf1 = ɵɵembeddedViewStart(1, 1, 1); - { - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'b'); - {} - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementAttribute(0, 'title', ɵɵbind(ctx.title)); - } - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 2, 1); - - const fixture = new ComponentFixture(App); - fixture.component.title = 'Hello'; - fixture.update(); - // initial binding - expect(fixture.html).toEqual(''); - // update DOM manually - fixture.hostElement.querySelector('b') !.setAttribute('title', 'Goodbye'); - - // refresh with same binding - fixture.update(); - expect(fixture.html).toEqual(''); - - // refresh again with same binding - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support host attribute bindings', () => { - let hostBindingDir: HostBindingDir; - - class HostBindingDir { - /* @HostBinding('attr.aria-label') */ - label = 'some label'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: function HostBindingDir_Factory() { - return hostBindingDir = new HostBindingDir(); - }, - hostBindings: function HostBindingDir_HostBindings( - rf: RenderFlags, ctx: any, elIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementAttribute(elIndex, 'aria-label', ɵɵbind(ctx.label)); - } - } - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['hostBindingDir', '']); - } - }, 1, 0, [HostBindingDir]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
        `); - - hostBindingDir !.label = 'other label'; - fixture.update(); - expect(fixture.html).toEqual(`
        `); - }); - }); - - describe('elementStyle', () => { - it('should support binding to styles', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵstyling(null, ['border-color']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstyleProp(0, ctx.color); - ɵɵstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.color = 'red'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.color = 'green'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.color = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support binding to styles with suffix', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵstyling(null, ['font-size']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstyleProp(0, ctx.time, 'px'); - ɵɵstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.time = '100'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = 200; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = 0; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.time = null; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - }); - - describe('class-based styling', () => { - it('should support CSS class toggle', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵstyling(['active']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵclassProp(0, ctx.class); - ɵɵstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual(''); - - // truthy values - fixture.component.class = 'a_string'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = 10; - fixture.update(); - expect(fixture.html).toEqual(''); - - // falsy values - fixture.component.class = ''; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = 0; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should work correctly with existing static classes', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span', [AttributeMarker.Classes, 'existing']); - ɵɵstyling(['existing', 'active']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵclassProp(1, ctx.class); - ɵɵstylingApply(); - } - }, 1); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should apply classes properly when nodes are components', () => { - const MyComp = createComponent('my-comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Comp Content'); - } - }, 1, 0, []); - - /** - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'my-comp'); - ɵɵstyling(['active']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵclassProp(0, ctx.class); - ɵɵstylingApply(); - } - }, 1, 0, [MyComp]); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html).toEqual('Comp Content'); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html).toEqual('Comp Content'); - }); - - it('should apply classes properly when nodes have LContainers', () => { - let structuralComp !: StructuralComp; - - class StructuralComp { - tmp !: TemplateRef; - - constructor(public vcr: ViewContainerRef) {} - - create() { this.vcr.createEmbeddedView(this.tmp); } - - static ngComponentDef = ɵɵdefineComponent({ - type: StructuralComp, - selectors: [['structural-comp']], - factory: () => structuralComp = - new StructuralComp(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'}, - consts: 1, - vars: 0, - template: (rf: RenderFlags, ctx: StructuralComp) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Comp Content'); - } - } - }); - } - - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Temp Content'); - } - } - - /** - * - * Temp Content - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelementStart(2, 'structural-comp'); - ɵɵstyling(['active']); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(1) as any; - ɵɵselect(2); - ɵɵclassProp(0, ctx.class); - ɵɵstylingApply(); - ɵɵelementProperty(2, 'tmp', ɵɵbind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.component.class = true; - fixture.update(); - expect(fixture.html) - .toEqual('Comp Content'); - - structuralComp.create(); - fixture.update(); - expect(fixture.html) - .toEqual('Comp ContentTemp Content'); - - fixture.component.class = false; - fixture.update(); - expect(fixture.html) - .toEqual('Comp ContentTemp Content'); - }); - - let mockClassDirective: DirWithClassDirective; - class DirWithClassDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirWithClassDirective, - selectors: [['', 'DirWithClass', '']], - factory: () => mockClassDirective = new DirWithClassDirective(), - inputs: {'klass': 'class'} - }); - - public classesVal: string = ''; - set klass(value: string) { this.classesVal = value; } - } - - let mockStyleDirective: DirWithStyleDirective; - class DirWithStyleDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirWithStyleDirective, - selectors: [['', 'DirWithStyle', '']], - factory: () => mockStyleDirective = new DirWithStyleDirective(), - inputs: {'style': 'style'} - }); - - public stylesVal: string = ''; - set style(value: string) { this.stylesVal = value; } - } - - it('should delegate initial classes to a [class] input binding if present on a directive on the same element', - () => { - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart( - 0, 'div', - ['DirWithClass', '', AttributeMarker.Classes, 'apple', 'orange', 'banana']); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstylingApply(); - } - }, 1, 0, [DirWithClassDirective]); - - const fixture = new ComponentFixture(App); - expect(mockClassDirective !.classesVal).toEqual('apple orange banana'); - }); - - it('should delegate initial styles to a [style] input binding if present on a directive on the same element', - () => { - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [ - 'DirWithStyle', '', AttributeMarker.Styles, 'width', '100px', 'height', '200px' - ]); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstylingApply(); - } - }, 1, 0, [DirWithStyleDirective]); - - const fixture = new ComponentFixture(App); - expect(mockStyleDirective !.stylesVal).toEqual('width:100px;height:200px'); - }); - - it('should update `[class]` and bindings in the provided directive if the input is matched', - () => { - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['DirWithClass']); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵclassMap('cucumber grape'); - ɵɵstylingApply(); - } - }, 1, 0, [DirWithClassDirective]); - - const fixture = new ComponentFixture(App); - expect(mockClassDirective !.classesVal).toEqual('cucumber grape'); - }); - - it('should update `[style]` and bindings in the provided directive if the input is matched', - () => { - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['DirWithStyle']); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstyleMap({width: '200px', height: '500px'}); - ɵɵstylingApply(); - } - }, 1, 0, [DirWithStyleDirective]); - - const fixture = new ComponentFixture(App); - expect(mockStyleDirective !.stylesVal).toEqual('width:200px;height:500px'); - }); - - it('should apply initial styling to the element that contains the directive with host styling', - () => { - class DirWithInitialStyling { - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirWithInitialStyling, - selectors: [['', 'DirWithInitialStyling', '']], - factory: () => new DirWithInitialStyling(), - hostBindings: function( - rf: RenderFlags, ctx: DirWithInitialStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs([ - 'title', 'foo', AttributeMarker.Classes, 'heavy', 'golden', - AttributeMarker.Styles, 'color', 'purple', 'font-weight', 'bold' - ]); - } - } - }); - - public classesVal: string = ''; - } - - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', [ - 'DirWithInitialStyling', '', AttributeMarker.Classes, 'big', - AttributeMarker.Styles, 'color', 'black', 'font-size', '200px' - ]); - } - }, 1, 0, [DirWithInitialStyling]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - const classes = target.getAttribute('class') !.split(/\s+/).sort(); - expect(classes).toEqual(['big', 'golden', 'heavy']); - - expect(target.getAttribute('title')).toEqual('foo'); - expect(target.style.getPropertyValue('color')).toEqual('black'); - expect(target.style.getPropertyValue('font-size')).toEqual('200px'); - expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); - }); - - it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', - () => { - let dirInstance: DirWithSingleStylingBindings; - /** - * - */ - class DirWithSingleStylingBindings { - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirWithSingleStylingBindings, - selectors: [['', 'DirWithSingleStylingBindings', '']], - factory: () => dirInstance = new DirWithSingleStylingBindings(), - hostBindings: function( - rf: RenderFlags, ctx: DirWithSingleStylingBindings, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs( - [AttributeMarker.Classes, 'def', AttributeMarker.Styles, 'width', '555px']); - ɵɵstyling(['xyz'], ['width', 'height']); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.width); - ɵɵstyleProp(1, ctx.height); - ɵɵclassProp(0, ctx.activateXYZClass); - ɵɵstylingApply(); - } - } - }); - - width: null|string = null; - height: null|string = null; - activateXYZClass: boolean = false; - } - - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', [ - 'DirWithSingleStylingBindings', '', AttributeMarker.Classes, 'abc', - AttributeMarker.Styles, 'width', '100px', 'height', '200px' - ]); - } - }, 1, 0, [DirWithSingleStylingBindings]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeFalsy(); - - dirInstance !.width = '444px'; - dirInstance !.height = '999px'; - dirInstance !.activateXYZClass = true; - fixture.update(); - - expect(target.style.getPropertyValue('width')).toEqual('444px'); - expect(target.style.getPropertyValue('height')).toEqual('999px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeTruthy(); - - dirInstance !.width = null; - dirInstance !.height = null; - fixture.update(); - - expect(target.style.getPropertyValue('width')).toEqual('100px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('abc')).toBeTruthy(); - expect(target.classList.contains('def')).toBeTruthy(); - expect(target.classList.contains('xyz')).toBeTruthy(); - }); - - it('should properly prioritize single style binding collisions when they exist on multiple directives', - () => { - let dir1Instance: Dir1WithStyle; - /** - * Directive with host props: - * [style.width] - */ - class Dir1WithStyle { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir1WithStyle, - selectors: [['', 'Dir1WithStyle', '']], - factory: () => dir1Instance = new Dir1WithStyle(), - hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyle, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.width); - ɵɵstylingApply(); - } - } - }); - width: null|string = null; - } - - let dir2Instance: Dir2WithStyle; - /** - * Directive with host props: - * [style.width] - * style="width:111px" - */ - class Dir2WithStyle { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir2WithStyle, - selectors: [['', 'Dir2WithStyle', '']], - factory: () => dir2Instance = new Dir2WithStyle(), - hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyle, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs([AttributeMarker.Styles, 'width', '111px']); - ɵɵstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵstyleProp(0, ctx.width); - ɵɵstylingApply(); - } - } - }); - width: null|string = null; - } - - /** - * Component with the following template: - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['Dir1WithStyle', '', 'Dir2WithStyle', '']); - ɵɵstyling(null, ['width']); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstyleProp(0, ctx.width); - ɵɵstylingApply(); - } - }, 1, 0, [Dir1WithStyle, Dir2WithStyle]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - fixture.component.width = '999px'; - dir1Instance !.width = '222px'; - dir2Instance !.width = '333px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('999px'); - - fixture.component.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('222px'); - - dir1Instance !.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('333px'); - - dir2Instance !.width = null; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - dir1Instance !.width = '666px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('666px'); - - fixture.component.width = '777px'; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('777px'); - }); - - it('should properly prioritize multi style binding collisions when they exist on multiple directives', - () => { - let dir1Instance: Dir1WithStyling; - /** - * Directive with host props: - * [style] - * [class] - */ - class Dir1WithStyling { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir1WithStyling, - selectors: [['', 'Dir1WithStyling', '']], - factory: () => dir1Instance = new Dir1WithStyling(), - hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵstyling(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleMap(ctx.stylesExp); - ɵɵclassMap(ctx.classesExp); - ɵɵstylingApply(); - } - } - }); - - classesExp: any = {}; - stylesExp: any = {}; - } - - let dir2Instance: Dir2WithStyling; - /** - * Directive with host props: - * [style] - * style="width:111px" - */ - class Dir2WithStyling { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Dir2WithStyling, - selectors: [['', 'Dir2WithStyling', '']], - factory: () => dir2Instance = new Dir2WithStyling(), - hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyling, elementIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs([AttributeMarker.Styles, 'width', '111px']); - ɵɵstyling(); - } - if (rf & RenderFlags.Update) { - ɵɵstyleMap(ctx.stylesExp); - ɵɵstylingApply(); - } - } - }); - - stylesExp: any = {}; - } - - /** - * Component with the following template: - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['Dir1WithStyling', '', 'Dir2WithStyling', '']); - ɵɵstyling(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵstyleMap(ctx.stylesExp); - ɵɵclassMap(ctx.classesExp); - ɵɵstylingApply(); - } - }, 1, 0, [Dir1WithStyling, Dir2WithStyling]); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.style.getPropertyValue('width')).toEqual('111px'); - - const compInstance = fixture.component; - compInstance.stylesExp = {width: '999px', height: null}; - compInstance.classesExp = {one: true, two: false}; - dir1Instance !.stylesExp = {width: '222px'}; - dir1Instance !.classesExp = {two: true, three: false}; - dir2Instance !.stylesExp = {width: '333px', height: '100px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('999px'); - expect(target.style.getPropertyValue('height')).toEqual('100px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - - compInstance.stylesExp = {}; - compInstance !.classesExp = {}; - dir1Instance !.stylesExp = {width: '222px', height: '200px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('222px'); - expect(target.style.getPropertyValue('height')).toEqual('200px'); - expect(target.classList.contains('one')).toBeFalsy(); - expect(target.classList.contains('two')).toBeTruthy(); - expect(target.classList.contains('three')).toBeFalsy(); - - dir1Instance !.stylesExp = {}; - dir1Instance !.classesExp = {}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('333px'); - expect(target.style.getPropertyValue('height')).toEqual('100px'); - expect(target.classList.contains('one')).toBeFalsy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - - dir2Instance !.stylesExp = {}; - compInstance.stylesExp = {height: '900px'}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('111px'); - expect(target.style.getPropertyValue('height')).toEqual('900px'); - - dir1Instance !.stylesExp = {width: '666px', height: '600px'}; - dir1Instance !.classesExp = {four: true, one: true}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('666px'); - expect(target.style.getPropertyValue('height')).toEqual('900px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - expect(target.classList.contains('four')).toBeTruthy(); - - compInstance.stylesExp = {width: '777px'}; - compInstance.classesExp = {four: false}; - fixture.update(); - expect(target.style.getPropertyValue('width')).toEqual('777px'); - expect(target.style.getPropertyValue('height')).toEqual('600px'); - expect(target.classList.contains('one')).toBeTruthy(); - expect(target.classList.contains('two')).toBeFalsy(); - expect(target.classList.contains('three')).toBeFalsy(); - expect(target.classList.contains('four')).toBeFalsy(); - }); - }); - - it('should properly handle and render interpolation for class attribute bindings', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - ɵɵstyling(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵselect(0); - ɵɵclassMap(ɵɵinterpolation2('-', ctx.name, '-', ctx.age, '-')); - ɵɵstylingApply(); - } - }, 1, 2); - - const fixture = new ComponentFixture(App); - const target = fixture.hostElement.querySelector('div') !; - expect(target.classList.contains('-fred-36-')).toBeFalsy(); - - fixture.component.name = 'fred'; - fixture.component.age = '36'; - fixture.update(); - - expect(target.classList.contains('-fred-36-')).toBeTruthy(); - }); - }); -}); - -describe('template data', () => { - - it('should re-use template data and node data', () => { - /** - * % if (condition) { - *
        - * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - expect((Template as any).ngPrivateData).toBeUndefined(); - - renderToHtml(Template, {condition: true}, 1); - - const oldTemplateData = (Template as any).ngPrivateData; - const oldContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; - const oldElementData = oldContainerData.tViews[0][HEADER_OFFSET]; - expect(oldContainerData).not.toBeNull(); - expect(oldElementData).not.toBeNull(); - - renderToHtml(Template, {condition: false}, 1); - renderToHtml(Template, {condition: true}, 1); - - const newTemplateData = (Template as any).ngPrivateData; - const newContainerData = (oldTemplateData as any).data[HEADER_OFFSET]; - const newElementData = oldContainerData.tViews[0][HEADER_OFFSET]; - expect(newTemplateData === oldTemplateData).toBe(true); - expect(newContainerData === oldContainerData).toBe(true); - expect(newElementData === oldElementData).toBe(true); - }); - }); describe('component styles', () => { diff --git a/packages/core/test/render3/ivy/BUILD.bazel b/packages/core/test/render3/ivy/BUILD.bazel index 39e0a8ce87..f5f2ecc499 100644 --- a/packages/core/test/render3/ivy/BUILD.bazel +++ b/packages/core/test/render3/ivy/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( "//packages:types", "//packages/core", "//packages/core/src/di/interface", + "@npm//reflect-metadata", ], ) diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index 6fb5771030..cb976aec2a 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -295,7 +295,7 @@ ivyEnabled && describe('render3 jit', () => { it('should compile ContentChild query with string predicate on a directive', () => { @Directive({selector: '[test]'}) class TestDirective { - @ContentChild('foo') foo: ElementRef|undefined; + @ContentChild('foo', {static: false}) foo: ElementRef|undefined; } expect((TestDirective as any).ngDirectiveDef.contentQueries).not.toBeNull(); @@ -317,7 +317,7 @@ ivyEnabled && describe('render3 jit', () => { @Directive({selector: '[test]'}) class TestDirective { - @ContentChild(SomeDir) dir: SomeDir|undefined; + @ContentChild(SomeDir, {static: false}) dir: SomeDir|undefined; } expect((TestDirective as any).ngDirectiveDef.contentQueries).not.toBeNull(); @@ -326,7 +326,7 @@ ivyEnabled && describe('render3 jit', () => { it('should compile ViewChild query on a component', () => { @Component({selector: 'test', template: ''}) class TestComponent { - @ViewChild('foo') foo: ElementRef|undefined; + @ViewChild('foo', {static: false}) foo: ElementRef|undefined; } expect((TestComponent as any).ngComponentDef.foo).not.toBeNull(); diff --git a/packages/core/test/render3/jit/directive_spec.ts b/packages/core/test/render3/jit/directive_spec.ts index 92e214afb7..3013e05a80 100644 --- a/packages/core/test/render3/jit/directive_spec.ts +++ b/packages/core/test/render3/jit/directive_spec.ts @@ -52,7 +52,8 @@ describe('jit directive helper functions', () => { descendants: false, first: false, isViewQuery: false, - read: undefined + read: undefined, + static: false, })).toEqual({ propertyName: 'propName', predicate: ['localRef'], @@ -69,7 +70,8 @@ describe('jit directive helper functions', () => { descendants: true, first: true, isViewQuery: true, - read: undefined + read: undefined, + static: false, })).toEqual({ propertyName: 'propName', predicate: ['foo', 'bar', 'baz'], diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index b0780aee98..2c69104aad 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core'; -import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, injectComponentFactoryResolver, ɵɵNgOnChangesFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {markDirty, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵprojection, ɵɵprojectionDef, ɵɵselect, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; +import {OnDestroy} from '../../src/core'; +import {AttributeMarker, ComponentTemplate, ɵɵNgOnChangesFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; +import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵselect, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgIf} from './common_with_def'; -import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util'; +import {ComponentFixture, createComponent} from './render_util'; describe('lifecycles', () => { @@ -32,7 +32,7 @@ describe('lifecycles', () => { beforeEach(() => { events = []; }); - let Comp = createOnInitComponent('comp', (rf: RenderFlags, ctx: any) => { + let Comp = createOnInitComponent('comp', (rf: RenderFlags) => { if (rf & RenderFlags.Create) { ɵɵprojectionDef(); ɵɵelementStart(0, 'div'); @@ -41,7 +41,7 @@ describe('lifecycles', () => { } }, 2); let Parent = createOnInitComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]); - let ProjectedComp = createOnInitComponent('projected', (rf: RenderFlags, ctx: any) => { + let ProjectedComp = createOnInitComponent('projected', (rf: RenderFlags) => { if (rf & RenderFlags.Create) { ɵɵtext(0, 'content'); } @@ -78,75 +78,6 @@ describe('lifecycles', () => { const directives = [Comp, Parent, ProjectedComp, Directive, NgIf]; - it('should call onInit method after inputs are set in creation mode (and not in update mode)', - () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(ctx.val)); - } - }, 1, 1, directives); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.val = '2'; - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should be called on root component in creation mode', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(events).toEqual(['comp']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(events).toEqual(['comp']); - }); - - it('should call parent onInit before child onInit', () => { - /** - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent', 'comp']); - }); - - it('should call all parent onInits across view before calling children onInits', () => { - /** - * - * - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 2); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent1', 'parent2', 'comp1', 'comp2']); - }); - - it('should call onInit every time a new view is created (if block)', () => { /** * % if (!skip) { @@ -183,2830 +114,5 @@ describe('lifecycles', () => { fixture.update(); expect(events).toEqual(['comp', 'comp']); }); - - - it('should call onInit every time a new view is created (ngIf)', () => { - - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, IfTemplate, 1, 0, 'comp', [AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - - fixture.component.showing = true; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.showing = false; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.showing = true; - fixture.update(); - expect(events).toEqual(['comp', 'comp']); - }); - - it('should call onInit for children of dynamically created components', () => { - let viewContainerComp !: ViewContainerComp; - - class ViewContainerComp { - constructor(public vcr: ViewContainerRef, public cfr: ComponentFactoryResolver) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: ViewContainerComp, - selectors: [['view-container-comp']], - factory: - () => viewContainerComp = new ViewContainerComp( - ɵɵdirectiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), - consts: 0, - vars: 0, - template: (rf: RenderFlags, ctx: ViewContainerComp) => {} - }); - } - - const DynamicComp = createComponent('dynamic-comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, [Comp]); - - const fixture = new ComponentFixture(ViewContainerComp); - expect(events).toEqual([]); - - viewContainerComp.vcr.createComponent( - viewContainerComp.cfr.resolveComponentFactory(DynamicComp)); - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should call onInit in hosts before their content children', () => { - /** - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelementStart(1, 'projected'); } - ɵɵelementEnd(); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'projected']); - }); - - it('should call onInit in host and its content children before next host', () => { - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelementStart(1, 'projected'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'comp'); - { ɵɵelementStart(3, 'projected'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 2); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 2); - } - }, 4, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp1', 'projected1', 'comp2', 'projected2']); - }); - - it('should be called on directives after component', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'dir']); - - fixture.update(); - expect(events).toEqual(['comp', 'dir']); - }); - - it('should be called on directives on an element', () => { - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['dir']); - - fixture.update(); - expect(events).toEqual(['dir']); - }); - - it('should call onInit properly in for loop', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵcontainer(1); - ɵɵelement(2, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 5); - ɵɵcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', j); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 0, directives); - - const fixture = new ComponentFixture(App); - // onInit is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual(['comp1', 'comp5', 'comp2', 'comp3', 'comp4']); - }); - - it('should call onInit properly in for loop with children', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵcontainer(1); - ɵɵelement(2, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 5); - ɵɵcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', j); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 0, directives); - - const fixture = new ComponentFixture(App); - // onInit is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual([ - 'parent1', 'parent5', 'parent2', 'comp2', 'parent3', 'comp3', 'parent4', 'comp4', 'comp1', - 'comp5' - ]); - }); - }); - - describe('doCheck', () => { - let events: string[]; - let allEvents: string[]; - - beforeEach(() => { - events = []; - allEvents = []; - }); - - let Comp = createDoCheckComponent('comp', (rf: RenderFlags, ctx: any) => {}); - let Parent = createDoCheckComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]); - - function createDoCheckComponent( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - ngDoCheck() { - events.push(name); - allEvents.push('check ' + name); - } - - ngOnInit() { allEvents.push('init ' + name); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), template, - consts: consts, - vars: vars, - directives: directives, - inputs: {val: 'val'} - }); - }; - } - - class Directive { - ngDoCheck() { events.push('dir'); } - - static ngDirectiveDef = ɵɵdefineDirective( - {type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()}); - } - - const directives = [Comp, Parent, Directive]; - - it('should call doCheck on every refresh', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp']); - - fixture.update(); - expect(events).toEqual(['comp', 'comp']); - }); - - it('should be called on root component', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(events).toEqual(['comp']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(events).toEqual(['comp', 'comp']); - }); - - it('should call parent doCheck before child doCheck', () => { - /** - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent', 'comp']); - }); - - it('should call ngOnInit before ngDoCheck if creation mode', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(allEvents).toEqual(['init comp', 'check comp']); - - fixture.update(); - expect(allEvents).toEqual(['init comp', 'check comp', 'check comp']); - }); - - it('should be called on directives after component', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'dir']); - - fixture.update(); - expect(events).toEqual(['comp', 'dir', 'comp', 'dir']); - }); - - it('should be called on directives on an element', () => { - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['dir']); - - fixture.update(); - expect(events).toEqual(['dir', 'dir']); - }); - - }); - - describe('afterContentInit', () => { - let events: string[]; - let allEvents: string[]; - - beforeEach(() => { - events = []; - allEvents = []; - }); - - let Comp = createAfterContentInitComp('comp', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵprojection(0); - } - }, 1); - - let Parent = createAfterContentInitComp('parent', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'comp'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(ctx.val)); - } - }, 2, 1, [Comp]); - - let ProjectedComp = createAfterContentInitComp('projected', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵprojection(0); - } - }, 1); - - function createAfterContentInitComp( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - val: string = ''; - ngAfterContentInit() { - events.push(`${name}${this.val}`); - allEvents.push(`${name}${this.val} init`); - } - ngAfterContentChecked() { allEvents.push(`${name}${this.val} check`); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), - consts: consts, - vars: vars, - inputs: {val: 'val'}, - template: template, - directives: directives - }); - }; - } - - class Directive { - ngAfterContentInit() { events.push('init'); } - ngAfterContentChecked() { events.push('check'); } - - static ngDirectiveDef = ɵɵdefineDirective( - {type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()}); - } - - function ForLoopWithChildrenTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - ɵɵcontainer(2); - ɵɵelementStart(3, 'parent'); - { ɵɵtext(4, 'content'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 4); - ɵɵcontainerRefreshStart(2); - { - for (let i = 2; i < 4; i++) { - let rf1 = ɵɵembeddedViewStart(0, 2, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', i); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - const directives = [Comp, Parent, ProjectedComp, Directive]; - - it('should be called only in creation mode', () => { - /** content */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp']); - - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should be called on root component in creation mode', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(events).toEqual(['comp']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(events).toEqual(['comp']); - }); - - it('should be called on every init (if blocks)', () => { - /** - * % if (!skip) { - * content - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 2, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp']); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.skip = false; - fixture.update(); - expect(events).toEqual(['comp', 'comp']); - }); - - it('should be called in parents before children', () => { - /** - * content - * - * parent template: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent', 'comp']); - }); - - it('should be called breadth-first in entire parent subtree before any children', () => { - /** - * content - * content - * - * parent template: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'parent'); - { ɵɵtext(3, 'content'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 2); - } - }, 4, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['parent1', 'parent2', 'comp1', 'comp2']); - }); - - it('should be called in projected components before their hosts', () => { - /** - * - * content - * - * - * parent template: - * - * - * projected comp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { - ɵɵelementStart(1, 'projected'); - { ɵɵtext(2, 'content'); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - }, 3, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['projected', 'parent', 'comp']); - }); - - it('should be called in projected components and hosts before children', () => { - /** - * - * content - * - * * - * content - * - * - * parent template: - * - * - * projected comp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { - ɵɵelementStart(1, 'projected'); - { ɵɵtext(2, 'content'); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - ɵɵelementStart(3, 'parent'); - { - ɵɵelementStart(4, 'projected'); - { ɵɵtext(5, 'content'); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 1); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 2); - ɵɵselect(4); - ɵɵelementProperty(4, 'val', 2); - } - }, 6, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['projected1', 'parent1', 'projected2', 'parent2', 'comp1', 'comp2']); - }); - - it('should be called in correct order in a for loop', () => { - /** - * content - * % for(let i = 2; i < 4; i++) { - * content - * % } - * content - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - ɵɵcontainer(2); - ɵɵelementStart(3, 'comp'); - { ɵɵtext(4, 'content'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 4); - ɵɵcontainerRefreshStart(2); - { - for (let i = 2; i < 4; i++) { - let rf1 = ɵɵembeddedViewStart(0, 2, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', i); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 5, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp4']); - }); - - it('should be called in correct order in a for loop with children', () => { - /** - * content - * % for(let i = 2; i < 4; i++) { - * content - * % } - * content - */ - - renderToHtml(ForLoopWithChildrenTemplate, {}, 5, 0, directives); - expect(events).toEqual( - ['parent2', 'comp2', 'parent3', 'comp3', 'parent1', 'parent4', 'comp1', 'comp4']); - }); - - describe('ngAfterContentChecked', () => { - - it('should be called every change detection run after afterContentInit', () => { - /** content */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵtext(1, 'content'); } - ɵɵelementEnd(); - } - }, 2, 0, directives); - - const fixture = new ComponentFixture(App); - expect(allEvents).toEqual(['comp init', 'comp check']); - - fixture.update(); - expect(allEvents).toEqual(['comp init', 'comp check', 'comp check']); - }); - - it('should be called on root component', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(allEvents).toEqual(['comp init', 'comp check']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(allEvents).toEqual(['comp init', 'comp check', 'comp check']); - }); - - }); - - describe('directives', () => { - it('should be called on directives after component', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'init', 'check']); - }); - - it('should be called on directives on an element', () => { - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, directives); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['init', 'check']); - }); - }); - }); - - describe('afterViewInit', () => { - let events: string[]; - let allEvents: string[]; - - beforeEach(() => { - events = []; - allEvents = []; - }); - - let Comp = createAfterViewInitComponent('comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - }, 2); - let Parent = createAfterViewInitComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]); - - let ProjectedComp = createAfterViewInitComponent('projected', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'content'); - } - }, 1); - - function createAfterViewInitComponent( - name: string, template: ComponentTemplate, consts: number, vars: number = 0, - directives: any[] = []) { - return class Component { - val: string = ''; - ngAfterViewInit() { - if (!this.val) this.val = ''; - events.push(`${name}${this.val}`); - allEvents.push(`${name}${this.val} init`); - } - ngAfterViewChecked() { allEvents.push(`${name}${this.val} check`); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - consts: consts, - vars: vars, - factory: () => new Component(), - inputs: {val: 'val'}, - template: template, - directives: directives - }); - }; - } - - class Directive { - ngAfterViewInit() { events.push('init'); } - ngAfterViewChecked() { events.push('check'); } - - static ngDirectiveDef = ɵɵdefineDirective( - {type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()}); - } - - const defs = [Comp, Parent, ProjectedComp, Directive]; - - it('should be called on init and not in update mode', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp']); - - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should be called on root component in creation mode', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(events).toEqual(['comp']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(events).toEqual(['comp']); - }); - - it('should be called every time a view is initialized (if block)', () => { - /* - * % if (!skip) { - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp']); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp']); - - fixture.component.skip = false; - fixture.update(); - expect(events).toEqual(['comp', 'comp']); - - }); - - it('should be called in children before parents', () => { - /** - * - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'parent']); - }); - - it('should be called for entire subtree before being called in any parent view comps', () => { - /** - * - * - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 2); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp1', 'comp2', 'parent1', 'parent2']); - - }); - - it('should be called in projected components before their hosts', () => { - /** - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelement(1, 'projected'); } - ɵɵelementEnd(); - } - }, 2, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['projected', 'comp']); - }); - - it('should call afterViewInit in content children and host before next host', () => { - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelement(1, 'projected'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'comp'); - { ɵɵelement(3, 'projected'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 2); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 2); - } - }, 4, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['projected1', 'comp1', 'projected2', 'comp2']); - }); - - it('should call afterViewInit in content children and hosts before parents', () => { - /* - * - * - * - */ - const ParentComp = createAfterViewInitComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelement(1, 'projected'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(ctx.val)); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', ɵɵbind(ctx.val)); - } - }, 2, 2, [Comp, ProjectedComp]); - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 2); - } - }, 2, 0, [ParentComp]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['projected1', 'comp1', 'projected2', 'comp2', 'parent1', 'parent2']); - }); - - it('should be called in correct order with for loops', () => { - /** - * - * % for (let i = 0; i < 4; i++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵcontainer(1); - ɵɵelement(2, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 4); - ɵɵcontainerRefreshStart(1); - { - for (let i = 2; i < 4; i++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', i); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp4']); - - }); - - it('should be called in correct order with for loops with children', () => { - /** - * - * % for(let i = 0; i < 4; i++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵcontainer(1); - ɵɵelement(2, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 4); - ɵɵcontainerRefreshStart(1); - { - for (let i = 2; i < 4; i++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', i); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual( - ['comp2', 'parent2', 'comp3', 'parent3', 'comp1', 'comp4', 'parent1', 'parent4']); - - }); - - describe('ngAfterViewChecked', () => { - - it('should call ngAfterViewChecked every update', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(allEvents).toEqual(['comp init', 'comp check']); - - fixture.update(); - expect(allEvents).toEqual(['comp init', 'comp check', 'comp check']); - }); - - it('should be called on root component', () => { - const comp = renderComponent(Comp, {hostFeatures: [LifecycleHooksFeature]}); - expect(allEvents).toEqual(['comp init', 'comp check']); - - markDirty(comp); - requestAnimationFrame.flush(); - expect(allEvents).toEqual(['comp init', 'comp check', 'comp check']); - }); - - it('should call ngAfterViewChecked with bindings', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(ctx.myVal)); - } - }, 1, 1, defs); - - const fixture = new ComponentFixture(App); - expect(allEvents).toEqual(['comp init', 'comp check']); - - fixture.component.myVal = 2; - fixture.update(); - expect(allEvents).toEqual(['comp init', 'comp check', 'comp2 check']); - }); - - it('should be called in correct order with for loops with children', () => { - /** - * - * % for(let i = 0; i < 4; i++) { - * - * % } - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵcontainer(1); - ɵɵelement(2, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 4); - ɵɵcontainerRefreshStart(1); - { - for (let i = 2; i < 4; i++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', i); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 0, defs); - - const fixture = new ComponentFixture(App); - expect(allEvents).toEqual([ - 'comp2 init', 'comp2 check', 'parent2 init', 'parent2 check', 'comp3 init', 'comp3 check', - 'parent3 init', 'parent3 check', 'comp1 init', 'comp1 check', 'comp4 init', 'comp4 check', - 'parent1 init', 'parent1 check', 'parent4 init', 'parent4 check' - ]); - - }); - - }); - - describe('directives', () => { - it('should be called on directives after component', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['comp', 'init', 'check']); - }); - - it('should be called on directives on an element', () => { - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual(['init', 'check']); - }); - }); - }); - - describe('onDestroy', () => { - let events: string[]; - - beforeEach(() => { events = []; }); - - let Comp = createOnDestroyComponent('comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵprojection(0); - } - }, 1); - let Parent = createOnDestroyComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]); - - function createOnDestroyComponent( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - val: string = ''; - ngOnDestroy() { events.push(`${name}${this.val}`); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), - consts: consts, - vars: vars, - inputs: {val: 'val'}, - template: template, - directives: directives - }); - }; - } - - let Grandparent = createOnDestroyComponent('grandparent', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - }, 1, 0, [Parent]); - - const ProjectedComp = createOnDestroyComponent('projected', (rf: RenderFlags, ctx: any) => {}); - - class Directive { - ngOnDestroy() { events.push('dir'); } - - static ngDirectiveDef = ɵɵdefineDirective( - {type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive()}); - } - - const defs = [Comp, Parent, Grandparent, ProjectedComp, Directive]; - - it('should call destroy when view is removed', () => { - /** - * % if (!skip) { - * - * % } - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp']); - }); - - it('should call destroy when multiple views are removed', () => { - /** - * % if (!skip) { - * - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 2, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵelement(1, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind('1')); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', ɵɵbind('2')); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp1', 'comp2']); - }); - - it('should be called in child components before parent components', () => { - /** - * % if (!skip) { - * - * % } - * - * parent template: - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp', 'parent']); - }); - - it('should be called bottom up with children nested 2 levels deep', () => { - /** - * % if (!skip) { - * - * % } - * - * grandparent template: - * parent template: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'grandparent'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['comp', 'parent', 'grandparent']); - }); - - it('should be called in projected components before their hosts', () => { - /** - * % if (!skip) { - * - * - * - * - * - * - * } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (!ctx.skip) { - let rf1 = ɵɵembeddedViewStart(0, 4, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelement(1, 'projected'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'comp'); - { ɵɵelement(3, 'projected'); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 1); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', 2); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', 2); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - fixture.component.skip = true; - fixture.update(); - expect(events).toEqual(['projected1', 'comp1', 'projected2', 'comp2']); - }); - - - it('should be called in consistent order if views are removed and re-added', () => { - /** - * % if (condition) { - * - * % if (condition2) { - * - * % } - * - * % } - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 3, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵcontainer(1); - ɵɵelement(2, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind('1')); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', ɵɵbind('3')); - ɵɵcontainerRefreshStart(1); - { - if (ctx.condition2) { - let rf2 = ɵɵembeddedViewStart(0, 1, 1); - if (rf2 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf2 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind('2')); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.component.condition2 = true; - fixture.update(); - - // remove all views - fixture.component.condition = false; - fixture.update(); - - /** - * Current angular will process in this same order (root is the top-level removed view): - * - * root.child (comp1 view) onDestroy: null - * root.child.next (container) -> embeddedView - * embeddedView.child (comp2 view) onDestroy: null - * embeddedView onDestroy: [comp2] - * root.child.next.next (comp3 view) onDestroy: null - * root onDestroy: [comp1, comp3] - */ - expect(events).toEqual(['comp2', 'comp1', 'comp3']); - - events = []; - // remove inner view - fixture.component.condition = true; - fixture.component.condition2 = false; - fixture.update(); - - // remove outer view - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['comp1', 'comp3']); - - events = []; - // restore both views - fixture.component.condition = true; - fixture.component.condition2 = true; - fixture.update(); - - // remove both views - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['comp2', 'comp1', 'comp3']); - }); - - it('should be called in every iteration of a destroyed for loop', () => { - /** - * % if (condition) { - * - * % for (let i = 2; i < len; i++) { - * - * % } - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 3, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵcontainer(1); - ɵɵelement(2, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind('1')); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', ɵɵbind('5')); - ɵɵcontainerRefreshStart(1); - { - for (let j = 2; j < ctx.len; j++) { - let rf2 = ɵɵembeddedViewStart(0, 1, 1); - if (rf2 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf2 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(j)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.component.len = 5; - fixture.update(); - - fixture.component.condition = false; - fixture.update(); - - /** - * Current angular will process in this same order (root is the top-level removed view): - * - * root.child (comp1 view) onDestroy: null - * root.child.next (container) -> embeddedView (children[0].data) - * embeddedView.child (comp2 view) onDestroy: null - * embeddedView onDestroy: [comp2] - * embeddedView.next.child (comp3 view) onDestroy: null - * embeddedView.next onDestroy: [comp3] - * embeddedView.next.next.child (comp4 view) onDestroy: null - * embeddedView.next.next onDestroy: [comp4] - * embeddedView.next.next -> container -> root - * root onDestroy: [comp1, comp5] - */ - expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']); - - events = []; - fixture.component.condition = true; - fixture.component.len = 4; - fixture.update(); - - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp5']); - - events = []; - fixture.component.condition = true; - fixture.component.len = 5; - fixture.update(); - - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']); - }); - - it('should call destroy properly if view also has listeners', () => { - /** - * % if (condition) { - * - * - * - * % } - */ - function Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 5, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { - ɵɵlistener('click', ctx.onClick.bind(ctx)); - ɵɵtext(1, 'Click me'); - } - ɵɵelementEnd(); - ɵɵelement(2, 'comp'); - ɵɵelementStart(3, 'button'); - { - ɵɵlistener('click', ctx.onClick.bind(ctx)); - ɵɵtext(4, 'Click me'); - } - ɵɵelementEnd(); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - } - - class App { - counter = 0; - condition = true; - onClick() { this.counter++; } - } - - const ctx: {counter: number} = new App(); - renderToHtml(Template, ctx, 1, 0, defs); - - const buttons = containerEl.querySelectorAll('button') !; - buttons[0].click(); - expect(ctx.counter).toEqual(1); - buttons[1].click(); - expect(ctx.counter).toEqual(2); - - renderToHtml(Template, {condition: false}, 1, 0, defs); - - buttons[0].click(); - buttons[1].click(); - expect(events).toEqual(['comp']); - expect(ctx.counter).toEqual(2); - }); - - it('should be called on directives after component', () => { - /** - * % if (condition) { - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(events).toEqual([]); - - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['comp', 'dir']); - - }); - - it('should be called on directives on an element', () => { - /** - * % if (condition) { - *
        - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(events).toEqual([]); - - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual(['dir']); - }); - - }); - - describe('onChanges', () => { - let events: ({type: string, name: string, [key: string]: any})[]; - - beforeEach(() => { events = []; }); - - /** - *
        - * - *
        - */ - const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - }, 2); - - /** - * - */ - const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(ctx.a)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(ctx.b)); - } - }, 1, 2, [Comp]); - - const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'content'); - } - }, 1); - - - function createOnChangesComponent( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - // @Input() val1: string; - // @Input('publicVal2') val2: string; - a: string = 'wasVal1BeforeMinification'; - b: string = 'wasVal2BeforeMinification'; - ngOnChanges(changes: SimpleChanges) { - if (changes.a && this.a !== changes.a.currentValue) { - throw Error( - `SimpleChanges invalid expected this.a ${this.a} to equal currentValue ${changes.a.currentValue}`); - } - if (changes.b && this.b !== changes.b.currentValue) { - throw Error( - `SimpleChanges invalid expected this.b ${this.b} to equal currentValue ${changes.b.currentValue}`); - } - events.push({type: 'onChanges', name: 'comp - ' + name, changes}); - } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), - consts: consts, - vars: vars, - inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template, - directives: directives, - features: [ɵɵNgOnChangesFeature()], - }); - }; - } - - class Directive { - // @Input() val1: string; - // @Input('publicVal2') val2: string; - a: string = 'wasVal1BeforeMinification'; - b: string = 'wasVal2BeforeMinification'; - ngOnChanges(changes: SimpleChanges) { - events.push({type: 'onChanges', name: 'dir - dir', changes}); - } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => new Directive(), - inputs: {a: 'val1', b: ['publicVal2', 'val2']}, - features: [ɵɵNgOnChangesFeature()], - }); - } - - const defs = [Comp, Parent, Directive, ProjectedComp]; - - it('should call onChanges method after inputs are set in creation and update mode', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(ctx.val1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(ctx.val2)); - } - }, 1, 2, defs); - - // First changes happen here. - const fixture = new ComponentFixture(App); - - events = []; - fixture.component.val1 = '1'; - fixture.component.val2 = 'a'; - fixture.update(); - expect(events).toEqual([{ - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange( - undefined, '1', false), // we cleared `events` above, this is the second change - 'val2': new SimpleChange(undefined, 'a', false), - } - }]); - - events = []; - fixture.component.val1 = '2'; - fixture.component.val2 = 'b'; - fixture.update(); - expect(events).toEqual([{ - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange('1', '2', false), - 'val2': new SimpleChange('a', 'b', false), - } - }]); - }); - - it('should call parent onChanges before child onChanges', () => { - /** - * - * parent temp: - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(ctx.val1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(ctx.val2)); - } - }, 1, 2, defs); - - const fixture = new ComponentFixture(App); - - // We're clearing events after the first change here - events = []; - fixture.component.val1 = '1'; - fixture.component.val2 = 'a'; - fixture.update(); - - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, '1', false), - 'val2': new SimpleChange(undefined, 'a', false), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, '1', false), - 'val2': new SimpleChange(undefined, 'a', false), - } - }, - ]); - }); - - it('should call all parent onChanges across view before calling children onChanges', () => { - /** - * - * - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - ɵɵselect(1); - ɵɵelementProperty(1, 'val1', ɵɵbind(2)); - ɵɵelementProperty(1, 'publicVal2', ɵɵbind(2)); - } - }, 2, 4, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - ]); - }); - - - it('should call onChanges every time a new view is created (if block)', () => { - /** - * % if (condition) { - * - * % } - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, defs); - - const fixture = new ComponentFixture(App); - - // Show the `comp` component, causing it to initialize. (first change is true) - fixture.component.condition = true; - fixture.update(); - expect(events).toEqual([{ - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }]); - - // Hide the `comp` component, no onChanges should fire - fixture.component.condition = false; - fixture.update(); - expect(events).toEqual([{ - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }]); - - // Show the `comp` component, it initializes again. (first change is true) - fixture.component.condition = true; - fixture.update(); - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - } - ]); - }); - - it('should call onChanges in hosts before their content children', () => { - /** - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelementStart(1, 'projected'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - ɵɵselect(1); - ɵɵelementProperty(1, 'val1', ɵɵbind(2)); - ɵɵelementProperty(1, 'publicVal2', ɵɵbind(2)); - } - }, 2, 4, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - projected', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - ]); - }); - - it('should call onChanges in host and its content children before next host', () => { - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'comp'); - { ɵɵelementStart(1, 'projected'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'comp'); - { ɵɵelementStart(3, 'projected'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - ɵɵselect(1); - ɵɵelementProperty(1, 'val1', ɵɵbind(2)); - ɵɵelementProperty(1, 'publicVal2', ɵɵbind(2)); - ɵɵselect(2); - ɵɵelementProperty(2, 'val1', ɵɵbind(3)); - ɵɵelementProperty(2, 'publicVal2', ɵɵbind(3)); - ɵɵselect(3); - ɵɵelementProperty(3, 'val1', ɵɵbind(4)); - ɵɵelementProperty(3, 'publicVal2', ɵɵbind(4)); - } - }, 4, 8, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - projected', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 3, true), - 'val2': new SimpleChange(undefined, 3, true), - } - }, - { - type: 'onChanges', - name: 'comp - projected', - changes: { - 'val1': new SimpleChange(undefined, 4, true), - 'val2': new SimpleChange(undefined, 4, true), - } - }, - ]); - }); - - it('should be called on directives after component', () => { - /** - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - } - }, 1, 2, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'dir - dir', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - ]); - - // Update causes no changes to be fired, since the bindings didn't change. - events = []; - fixture.update(); - expect(events).toEqual([]); - - }); - - it('should be called on directives on an element', () => { - /** - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - } - }, 1, 2, defs); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([{ - type: 'onChanges', - name: 'dir - dir', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }]); - - events = []; - fixture.update(); - expect(events).toEqual([]); - }); - - it('should call onChanges properly in for loop', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵcontainer(1); - ɵɵelement(2, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - ɵɵselect(2); - ɵɵelementProperty(2, 'val1', ɵɵbind(5)); - ɵɵelementProperty(2, 'publicVal2', ɵɵbind(5)); - ɵɵcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(j)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(j)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 4, defs); - - const fixture = new ComponentFixture(App); - - // onChanges is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 5, true), - 'val2': new SimpleChange(undefined, 5, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 3, true), - 'val2': new SimpleChange(undefined, 3, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 4, true), - 'val2': new SimpleChange(undefined, 4, true), - } - }, - ]); - }); - - it('should call onChanges properly in for loop with children', () => { - /** - * - * % for (let j = 2; j < 5; j++) { - * - * % } - * - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵcontainer(1); - ɵɵelement(2, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(1)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(1)); - ɵɵselect(2); - ɵɵelementProperty(2, 'val1', ɵɵbind(5)); - ɵɵelementProperty(2, 'publicVal2', ɵɵbind(5)); - ɵɵcontainerRefreshStart(1); - { - for (let j = 2; j < 5; j++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 2); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'val1', ɵɵbind(j)); - ɵɵelementProperty(0, 'publicVal2', ɵɵbind(j)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 4, defs); - - const fixture = new ComponentFixture(App); - - // onChanges is called top to bottom, so top level comps (1 and 5) are called - // before the comps inside the for loop's embedded view (2, 3, and 4) - expect(events).toEqual([ - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 5, true), - 'val2': new SimpleChange(undefined, 5, true), - } - }, - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 2, true), - 'val2': new SimpleChange(undefined, 2, true), - } - }, - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 3, true), - 'val2': new SimpleChange(undefined, 3, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 3, true), - 'val2': new SimpleChange(undefined, 3, true), - } - }, - { - type: 'onChanges', - name: 'comp - parent', - changes: { - 'val1': new SimpleChange(undefined, 4, true), - 'val2': new SimpleChange(undefined, 4, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 4, true), - 'val2': new SimpleChange(undefined, 4, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 1, true), - 'val2': new SimpleChange(undefined, 1, true), - } - }, - { - type: 'onChanges', - name: 'comp - comp', - changes: { - 'val1': new SimpleChange(undefined, 5, true), - 'val2': new SimpleChange(undefined, 5, true), - } - }, - ]); - }); - - it('should not call onChanges if props are set directly', () => { - let events: SimpleChanges[] = []; - let compInstance: MyComp; - class MyComp { - value = 0; - - ngOnChanges(changes: SimpleChanges) { events.push(changes); } - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - factory: () => { - // Capture the instance so we can test setting the property directly - compInstance = new MyComp(); - return compInstance; - }, - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.a)); - } - }, - selectors: [['my-comp']], - inputs: { - value: 'value', - }, - consts: 1, - vars: 1, - }); - } - - /** - * - */ - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'value', ɵɵbind(1)); - } - }, 1, 1, [MyComp]); - - const fixture = new ComponentFixture(App); - events = []; - - // Try setting the property directly - compInstance !.value = 2; - - fixture.update(); - expect(events).toEqual([]); - }); - - }); - - describe('hook order', () => { - let events: string[]; - - beforeEach(() => { events = []; }); - - function createAllHooksComponent( - name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, - directives: any[] = []) { - return class Component { - val: string = ''; - - ngOnChanges() { events.push(`changes ${name}${this.val}`); } - - ngOnInit() { events.push(`init ${name}${this.val}`); } - ngDoCheck() { events.push(`check ${name}${this.val}`); } - - ngAfterContentInit() { events.push(`contentInit ${name}${this.val}`); } - ngAfterContentChecked() { events.push(`contentCheck ${name}${this.val}`); } - - ngAfterViewInit() { events.push(`viewInit ${name}${this.val}`); } - ngAfterViewChecked() { events.push(`viewCheck ${name}${this.val}`); } - - static ngComponentDef = ɵɵdefineComponent({ - type: Component, - selectors: [[name]], - factory: () => new Component(), - consts: consts, - vars: vars, - inputs: {val: 'val'}, template, - directives: directives, - features: [ɵɵNgOnChangesFeature()], - }); - }; - } - - it('should call all hooks in correct order', () => { - const Comp = createAllHooksComponent('comp', (rf: RenderFlags, ctx: any) => {}); - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - ɵɵelement(1, 'comp'); - } - // This template function is a little weird in that the `elementProperty` calls - // below are directly setting values `1` and `2`, where normally there would be - // a call to `bind()` that would do the work of seeing if something changed. - // This means when `fixture.update()` is called below, ngOnChanges should fire, - // even though the *value* itself never changed. - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 2); - } - }, 2, 0, [Comp]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes comp1', 'init comp1', 'check comp1', 'changes comp2', 'init comp2', 'check comp2', - 'contentInit comp1', 'contentCheck comp1', 'contentInit comp2', 'contentCheck comp2', - 'viewInit comp1', 'viewCheck comp1', 'viewInit comp2', 'viewCheck comp2' - ]); - - events = []; - fixture.update(); // Changes are made due to lack of `bind()` call in template fn. - expect(events).toEqual([ - 'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1', - 'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2' - ]); - }); - - it('should call all hooks in correct order with children', () => { - const Comp = createAllHooksComponent('comp', (rf: RenderFlags, ctx: any) => {}); - - /** */ - const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(ctx.val)); - } - }, 1, 1, [Comp]); - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent'); - ɵɵelement(1, 'parent'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', 1); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', 2); - } - }, 2, 0, [Parent]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes parent1', 'init parent1', 'check parent1', - 'changes parent2', 'init parent2', 'check parent2', - 'contentInit parent1', 'contentCheck parent1', 'contentInit parent2', - 'contentCheck parent2', 'changes comp1', 'init comp1', - 'check comp1', 'contentInit comp1', 'contentCheck comp1', - 'viewInit comp1', 'viewCheck comp1', 'changes comp2', - 'init comp2', 'check comp2', 'contentInit comp2', - 'contentCheck comp2', 'viewInit comp2', 'viewCheck comp2', - 'viewInit parent1', 'viewCheck parent1', 'viewInit parent2', - 'viewCheck parent2' - ]); - - events = []; - fixture.update(); - expect(events).toEqual([ - 'changes parent1', 'check parent1', 'changes parent2', 'check parent2', - 'contentCheck parent1', 'contentCheck parent2', 'check comp1', 'contentCheck comp1', - 'viewCheck comp1', 'check comp2', 'contentCheck comp2', 'viewCheck comp2', - 'viewCheck parent1', 'viewCheck parent2' - ]); - - }); - - // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-ng - it('should call all hooks in correct order with view and content', () => { - const Content = createAllHooksComponent('content', (rf: RenderFlags, ctx: any) => {}); - - const View = createAllHooksComponent('view', (rf: RenderFlags, ctx: any) => {}); - - /** */ - const Parent = createAllHooksComponent('parent', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵprojection(0); - ɵɵelement(1, 'view'); - } - if (rf & RenderFlags.Update) { - ɵɵselect(1); - ɵɵelementProperty(1, 'val', ɵɵbind(ctx.val)); - } - }, 2, 1, [View]); - - /** - * - * - * - * - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'parent'); - { ɵɵelement(1, 'content'); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'parent'); - { ɵɵelement(3, 'content'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'val', ɵɵbind(1)); - ɵɵselect(1); - ɵɵelementProperty(1, 'val', ɵɵbind(1)); - ɵɵselect(2); - ɵɵelementProperty(2, 'val', ɵɵbind(2)); - ɵɵselect(3); - ɵɵelementProperty(3, 'val', ɵɵbind(2)); - } - }, 4, 4, [Parent, Content]); - - const fixture = new ComponentFixture(App); - expect(events).toEqual([ - 'changes parent1', 'init parent1', - 'check parent1', 'changes content1', - 'init content1', 'check content1', - 'changes parent2', 'init parent2', - 'check parent2', 'changes content2', - 'init content2', 'check content2', - 'contentInit content1', 'contentCheck content1', - 'contentInit parent1', 'contentCheck parent1', - 'contentInit content2', 'contentCheck content2', - 'contentInit parent2', 'contentCheck parent2', - 'changes view1', 'init view1', - 'check view1', 'contentInit view1', - 'contentCheck view1', 'viewInit view1', - 'viewCheck view1', 'changes view2', - 'init view2', 'check view2', - 'contentInit view2', 'contentCheck view2', - 'viewInit view2', 'viewCheck view2', - 'viewInit content1', 'viewCheck content1', - 'viewInit parent1', 'viewCheck parent1', - 'viewInit content2', 'viewCheck content2', - 'viewInit parent2', 'viewCheck parent2' - ]); - - events = []; - fixture.update(); - expect(events).toEqual([ - 'check parent1', 'check content1', 'check parent2', 'check content2', - 'contentCheck content1', 'contentCheck parent1', 'contentCheck content2', - 'contentCheck parent2', 'check view1', 'contentCheck view1', 'viewCheck view1', - 'check view2', 'contentCheck view2', 'viewCheck view2', 'viewCheck content1', - 'viewCheck parent1', 'viewCheck content2', 'viewCheck parent2' - ]); - - }); - - }); - - describe('non-regression', () => { - - it('should call lifecycle hooks for directives active on ', () => { - let destroyed = false; - - class OnDestroyDirective implements OnDestroy { - ngOnDestroy() { destroyed = true; } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OnDestroyDirective, - selectors: [['', 'onDestroyDirective', '']], - factory: () => new OnDestroyDirective() - }); - } - - - function conditionTpl(rf: RenderFlags, ctx: Cmpt) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, null, 0, 1, 'ng-template', [AttributeMarker.Bindings, 'onDestroyDirective']); - } - } - - /** - * - * - * - */ - function cmptTpl(rf: RenderFlags, cmpt: Cmpt) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, conditionTpl, 1, 1, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(cmpt.showing)); - } - } - - class Cmpt { - showing = true; - static ngComponentDef = ɵɵdefineComponent({ - type: Cmpt, - factory: () => new Cmpt(), - selectors: [['cmpt']], - consts: 1, - vars: 1, - template: cmptTpl, - directives: [NgIf, OnDestroyDirective] - }); - } - - const fixture = new ComponentFixture(Cmpt); - expect(destroyed).toBeFalsy(); - - fixture.component.showing = false; - fixture.update(); - expect(destroyed).toBeTruthy(); - }); - }); - }); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts deleted file mode 100644 index d7efadec1a..0000000000 --- a/packages/core/test/render3/properties_spec.ts +++ /dev/null @@ -1,621 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {EventEmitter} from '@angular/core'; - -import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵlistener, ɵɵload, ɵɵreference, ɵɵtext, ɵɵtextBinding} from '../../src/render3/instructions/all'; -import {RenderFlags} from '../../src/render3/interfaces/definition'; - -import {ComponentFixture, createComponent, renderToHtml} from './render_util'; - -describe('elementProperty', () => { - - it('should support bindings to properties', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'testId'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'otherId'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - it('should support creation time bindings to properties', () => { - function expensive(ctx: string): any { - if (ctx === 'cheapId') { - return ctx; - } else { - throw 'Too expensive!'; - } - } - - function Template(rf: RenderFlags, ctx: string) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - ɵɵelementProperty(0, 'id', expensive(ctx)); - } - } - - expect(renderToHtml(Template, 'cheapId', 1)).toEqual(''); - expect(renderToHtml(Template, 'expensiveId', 1)).toEqual(''); - }); - - it('should support interpolation for properties', () => { - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵinterpolation1('_', ctx.id, '_')); - } - }, 1, 1); - - const fixture = new ComponentFixture(App); - fixture.component.id = 'testId'; - fixture.update(); - expect(fixture.html).toEqual(''); - - fixture.component.id = 'otherId'; - fixture.update(); - expect(fixture.html).toEqual(''); - }); - - describe('input properties', () => { - let button: MyButton; - let otherDir: OtherDir; - let otherDisabledDir: OtherDisabledDir; - let idDir: IdDir; - - class MyButton { - // TODO(issue/24571): remove '!'. - disabled !: boolean; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyButton, - selectors: [['', 'myButton', '']], - factory: () => button = new MyButton(), - inputs: {disabled: 'disabled'} - }); - } - - class OtherDir { - // TODO(issue/24571): remove '!'. - id !: number; - clickStream = new EventEmitter(); - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OtherDir, - selectors: [['', 'otherDir', '']], - factory: () => otherDir = new OtherDir(), - inputs: {id: 'id'}, - outputs: {clickStream: 'click'} - }); - } - - class OtherDisabledDir { - // TODO(issue/24571): remove '!'. - disabled !: boolean; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: OtherDisabledDir, - selectors: [['', 'otherDisabledDir', '']], - factory: () => otherDisabledDir = new OtherDisabledDir(), - inputs: {disabled: 'disabled'} - }); - } - - class IdDir { - // TODO(issue/24571): remove '!'. - idNumber !: string; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: IdDir, - selectors: [['', 'idDir', '']], - factory: () => idDir = new IdDir(), - inputs: {idNumber: 'id'} - }); - } - - - const deps = [MyButton, OtherDir, OtherDisabledDir, IdDir]; - - it('should check input properties before setting (directives)', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '', 'myButton', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 2, deps); - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.component.id = 0; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - expect(otherDir !.id).toEqual(0); - - fixture.component.isDisabled = false; - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - expect(otherDir !.id).toEqual(1); - }); - - it('should support mixed element properties and input properties', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['myButton', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 2, deps); - - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.component.id = 0; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - - fixture.component.isDisabled = false; - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - }); - - it('should check that property is not an input property before setting (component)', () => { - let comp: Comp; - - class Comp { - // TODO(issue/24571): remove '!'. - id !: number; - - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - consts: 0, - vars: 0, - template: function(rf: RenderFlags, ctx: any) {}, - factory: () => comp = new Comp(), - inputs: {id: 'id'} - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 1, 1, [Comp]); - - const fixture = new ComponentFixture(App); - fixture.component.id = 1; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(comp !.id).toEqual(1); - - fixture.component.id = 2; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(comp !.id).toEqual(2); - }); - - it('should support two input properties with the same name', () => { - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['myButton', '', 'otherDisabledDir', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'disabled', ɵɵbind(ctx.isDisabled)); - } - }, 2, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.isDisabled = true; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(true); - expect(otherDisabledDir !.disabled).toEqual(true); - - fixture.component.isDisabled = false; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(button !.disabled).toEqual(false); - expect(otherDisabledDir !.disabled).toEqual(false); - }); - - it('should set input property if there is an output first', () => { - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '']); - { - ɵɵlistener('click', () => ctx.onClick()); - ɵɵtext(1, 'Click me'); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id)); - } - }, 2, 1, deps); - - const fixture = new ComponentFixture(App); - let counter = 0; - fixture.component.id = 1; - fixture.component.onClick = () => counter++; - fixture.update(); - expect(fixture.html).toEqual(``); - expect(otherDir !.id).toEqual(1); - - otherDir !.clickStream.next(); - expect(counter).toEqual(1); - - fixture.component.id = 2; - fixture.update(); - fixture.html; - expect(otherDir !.id).toEqual(2); - }); - - it('should support unrelated element properties at same index in if-else block', () => { - /** - * // inputs: {'id': [0, 'idNumber']} - * % if (condition) { - * // inputs: null - * % } else { - * // inputs: {'id': [0, 'id']} - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['idDir', '']); - { ɵɵtext(1, 'Click me'); } - ɵɵelementEnd(); - ɵɵcontainer(2); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id1)); - ɵɵcontainerRefreshStart(2); - { - if (ctx.condition) { - let rf0 = ɵɵembeddedViewStart(0, 2, 1); - if (rf0 & RenderFlags.Create) { - ɵɵelementStart(0, 'button'); - { ɵɵtext(1, 'Click me too'); } - ɵɵelementEnd(); - } - if (rf0 & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id2)); - } - ɵɵembeddedViewEnd(); - } else { - let rf1 = ɵɵembeddedViewStart(1, 2, 1); - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'button', ['otherDir', '']); - { ɵɵtext(1, 'Click me too'); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(ctx.id3)); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 3, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.component.id1 = 'one'; - fixture.component.id2 = 'two'; - fixture.component.id3 = 3; - fixture.update(); - expect(fixture.html) - .toEqual(``); - expect(idDir !.idNumber).toEqual('one'); - - fixture.component.condition = false; - fixture.component.id1 = 'four'; - fixture.update(); - expect(fixture.html) - .toEqual(``); - expect(idDir !.idNumber).toEqual('four'); - expect(otherDir !.id).toEqual(3); - }); - - }); - - describe('attributes and input properties', () => { - let myDir: MyDir; - class MyDir { - // TODO(issue/24571): remove '!'. - role !: string; - // TODO(issue/24571): remove '!'. - direction !: string; - changeStream = new EventEmitter(); - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDir, - selectors: [['', 'myDir', '']], - factory: () => myDir = new MyDir(), - inputs: {role: 'role', direction: 'dir'}, - outputs: {changeStream: 'change'}, - exportAs: ['myDir'] - }); - } - - let dirB: MyDirB; - class MyDirB { - // TODO(issue/24571): remove '!'. - roleB !: string; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: MyDirB, - selectors: [['', 'myDirB', '']], - factory: () => dirB = new MyDirB(), - inputs: {roleB: 'role'} - }); - } - - const deps = [MyDir, MyDirB]; - - it('should set input property based on attribute if existing', () => { - - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('button'); - }); - - it('should set input property and attribute if both defined', () => { - - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'role', ɵɵbind(ctx.role)); - } - }, 1, 1, deps); - - const fixture = new ComponentFixture(App); - fixture.component.role = 'listbox'; - fixture.update(); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('listbox'); - - fixture.component.role = 'button'; - fixture.update(); - expect(myDir !.role).toEqual('button'); - }); - - it('should set two directive input properties based on same attribute', () => { - - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', '', 'myDirB', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('button'); - expect(dirB !.roleB).toEqual('button'); - }); - - it('should process two attributes on same directive', () => { - - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('button'); - expect(myDir !.direction).toEqual('rtl'); - }); - - it('should process attributes and outputs properly together', () => { - - /**
        */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['role', 'button', 'myDir', '']); - { ɵɵlistener('change', () => ctx.onChange()); } - ɵɵelementEnd(); - } - }, 1, 0, deps); - - const fixture = new ComponentFixture(App); - let counter = 0; - fixture.component.onChange = () => counter++; - fixture.update(); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('button'); - - myDir !.changeStream.next(); - expect(counter).toEqual(1); - }); - - it('should process attributes properly for directives with later indices', () => { - - /** - *
        - *
        - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'dir', 'rtl', 'myDir', '']); - ɵɵelement(1, 'div', ['role', 'listbox', 'myDirB', '']); - } - }, 2, 0, deps); - - const fixture = new ComponentFixture(App); - expect(fixture.html) - .toEqual( - `
        `); - expect(myDir !.role).toEqual('button'); - expect(myDir !.direction).toEqual('rtl'); - expect(dirB !.roleB).toEqual('listbox'); - }); - - it('should support attributes at same index inside an if-else block', () => { - /** - *
        // initialInputs: [['role', 'listbox']] - * - * % if (condition) { - *
        // initialInputs: [['role', 'button']] - * % } else { - *
        // initialInputs: [null] - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'listbox', 'myDir', '']); - ɵɵcontainer(1); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(1); - { - if (ctx.condition) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDirB', '']); - } - ɵɵembeddedViewEnd(); - } else { - let rf2 = ɵɵembeddedViewStart(1, 1, 0); - if (rf2 & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'menu']); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 2, 0, deps); - - const fixture = new ComponentFixture(App); - fixture.component.condition = true; - fixture.update(); - expect(fixture.html) - .toEqual(`
        `); - expect(myDir !.role).toEqual('listbox'); - expect(dirB !.roleB).toEqual('button'); - expect((dirB !as any).role).toBeUndefined(); - - fixture.component.condition = false; - fixture.update(); - expect(fixture.html).toEqual(`
        `); - expect(myDir !.role).toEqual('listbox'); - }); - - it('should process attributes properly inside a for loop', () => { - - class Comp { - static ngComponentDef = ɵɵdefineComponent({ - type: Comp, - selectors: [['comp']], - consts: 3, - vars: 1, - /**
        {{ dir.role }} */ - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['role', 'button', 'myDir', ''], ['dir', 'myDir']); - ɵɵtext(2); - } - if (rf & RenderFlags.Update) { - const tmp = ɵɵreference(1) as any; - ɵɵtextBinding(2, ɵɵbind(tmp.role)); - } - }, - factory: () => new Comp(), - directives: () => [MyDir] - }); - } - - /** - * % for (let i = 0; i < 3; i++) { - * - * % } - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵcontainer(0); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(0); - { - for (let i = 0; i < 2; i++) { - let rf1 = ɵɵembeddedViewStart(0, 1, 0); - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'comp'); - } - ɵɵembeddedViewEnd(); - } - } - ɵɵcontainerRefreshEnd(); - } - }, 1, 0, [Comp]); - - const fixture = new ComponentFixture(App); - expect(fixture.html) - .toEqual( - `
        button
        button
        `); - }); - - }); - -}); diff --git a/packages/core/test/render3/pure_function_spec.ts b/packages/core/test/render3/pure_function_spec.ts index 2824d8ee2a..42f7a17476 100644 --- a/packages/core/test/render3/pure_function_spec.ts +++ b/packages/core/test/render3/pure_function_spec.ts @@ -5,381 +5,13 @@ * 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 {AttributeMarker, ɵɵdefineComponent, ɵɵtemplate} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵnextContext} from '../../src/render3/instructions/all'; +import {ɵɵdefineComponent} from '../../src/render3/index'; +import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {ɵɵpureFunction1, ɵɵpureFunction2, ɵɵpureFunction3, ɵɵpureFunction4, ɵɵpureFunction5, ɵɵpureFunction6, ɵɵpureFunction7, ɵɵpureFunction8, ɵɵpureFunctionV} from '../../src/render3/pure_function'; -import {ComponentFixture, createComponent, getDirectiveOnNode, renderToHtml} from '../../test/render3/render_util'; - -import {NgIf} from './common_with_def'; - -describe('array literals', () => { - let myComp: MyComp; - - class MyComp { - // TODO(issue/24571): remove '!'. - names !: string[]; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComp, - selectors: [['my-comp']], - factory: function MyComp_Factory() { return myComp = new MyComp(); }, - consts: 0, - vars: 0, - template: function MyComp_Template(rf: RenderFlags, ctx: MyComp) {}, - inputs: {names: 'names'} - }); - } - - const directives = [MyComp]; - - it('should support an array literal with a binding', () => { - const e0_ff = (v: any) => ['Nancy', v, 'Bess']; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction1(1, e0_ff, ctx.customName))); - } - }, 1, 3, directives); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.update(); - const firstArray = myComp !.names; - expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess']); - - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess']); - expect(firstArray).toBe(myComp !.names); - - fixture.component.customName = 'Hannah'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Hannah', 'Bess']); - - // Identity must change if binding changes - expect(firstArray).not.toBe(myComp !.names); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - myComp !.names = ['should not be overwritten']; - fixture.update(); - expect(myComp !.names).toEqual(['should not be overwritten']); - }); - - it('should support array literals in dynamic views', () => { - const e0_ff = (v: any) => ['Nancy', v, 'Bess']; - - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - const comp = ɵɵnextContext(); - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction1(1, e0_ff, comp.customName))); - } - } - - /** - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, IfTemplate, 1, 3, 'my-comp', - [AttributeMarker.Bindings, 'names', AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - } - }, 1, 1, [MyComp, NgIf]); - - const fixture = new ComponentFixture(App); - fixture.component.showing = true; - fixture.component.customName = 'Carson'; - fixture.update(); - - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess']); - }); - - it('should support multiple array literals passed through to one node', () => { - let manyPropComp: ManyPropComp; - - class ManyPropComp { - // TODO(issue/24571): remove '!'. - names1 !: string[]; - // TODO(issue/24571): remove '!'. - names2 !: string[]; - - static ngComponentDef = ɵɵdefineComponent({ - type: ManyPropComp, - selectors: [['many-prop-comp']], - factory: function ManyPropComp_Factory() { return manyPropComp = new ManyPropComp(); }, - consts: 0, - vars: 0, - template: function ManyPropComp_Template(rf: RenderFlags, ctx: ManyPropComp) {}, - inputs: {names1: 'names1', names2: 'names2'} - }); - } - - const e0_ff = (v: any) => ['Nancy', v]; - const e0_ff_1 = (v: any) => [v]; - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'many-prop-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names1', ɵɵbind(ɵɵpureFunction1(2, e0_ff, ctx.customName))); - ɵɵelementProperty(0, 'names2', ɵɵbind(ɵɵpureFunction1(4, e0_ff_1, ctx.customName2))); - } - }, 1, 6, [ManyPropComp]); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.component.customName2 = 'George'; - fixture.update(); - expect(manyPropComp !.names1).toEqual(['Nancy', 'Carson']); - expect(manyPropComp !.names2).toEqual(['George']); - - fixture.component.customName = 'George'; - fixture.component.customName2 = 'Carson'; - fixture.update(); - expect(manyPropComp !.names1).toEqual(['Nancy', 'George']); - expect(manyPropComp !.names2).toEqual(['Carson']); - }); - - it('should support an array literals inside fn calls', () => { - let myComps: MyComp[] = []; - - const e0_ff = (v: any) => ['Nancy', v]; - - /** */ - class ParentComp { - customName = 'Bess'; - - someFn(arr: string[]): string[] { - arr[0] = arr[0].toUpperCase(); - return arr; - } - - static ngComponentDef = ɵɵdefineComponent({ - type: ParentComp, - selectors: [['parent-comp']], - factory: () => new ParentComp(), - consts: 1, - vars: 3, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'my-comp'); - myComps.push(getDirectiveOnNode(0)); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ctx.someFn(ɵɵpureFunction1(1, e0_ff, ctx.customName)))); - } - }, - directives: directives - }); - } - - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent-comp'); - ɵɵelement(1, 'parent-comp'); - } - }, 2, 0, [ParentComp]); - - const fixture = new ComponentFixture(App); - const firstArray = myComps[0].names; - const secondArray = myComps[1].names; - expect(firstArray).toEqual(['NANCY', 'Bess']); - expect(secondArray).toEqual(['NANCY', 'Bess']); - expect(firstArray).not.toBe(secondArray); - - fixture.update(); - expect(firstArray).toEqual(['NANCY', 'Bess']); - expect(secondArray).toEqual(['NANCY', 'Bess']); - expect(firstArray).toBe(myComps[0].names); - expect(secondArray).toBe(myComps[1].names); - }); - - it('should support an array literal with more than 1 binding', () => { - const e0_ff = (v1: any, v2: any) => ['Nancy', v1, 'Bess', v2]; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ɵɵpureFunction2(1, e0_ff, ctx.customName, ctx.customName2))); - } - }, 1, 4, directives); - - const fixture = new ComponentFixture(App); - fixture.component.customName = 'Carson'; - fixture.component.customName2 = 'Hannah'; - fixture.update(); - const firstArray = myComp !.names; - expect(firstArray).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); - - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Carson', 'Bess', 'Hannah']); - expect(firstArray).toBe(myComp !.names); - - fixture.component.customName = 'George'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'George', 'Bess', 'Hannah']); - expect(firstArray).not.toBe(myComp !.names); - - fixture.component.customName = 'Frank'; - fixture.component.customName2 = 'Ned'; - fixture.update(); - expect(myComp !.names).toEqual(['Nancy', 'Frank', 'Bess', 'Ned']); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - myComp !.names = ['should not be overwritten']; - fixture.update(); - expect(myComp !.names).toEqual(['should not be overwritten']); - }); - - it('should work up to 8 bindings', () => { - let f3Comp: MyComp; - let f4Comp: MyComp; - let f5Comp: MyComp; - let f6Comp: MyComp; - let f7Comp: MyComp; - let f8Comp: MyComp; +import {ɵɵpureFunction2} from '../../src/render3/pure_function'; +import {getDirectiveOnNode, renderToHtml} from '../../test/render3/render_util'; - const e0_ff = (v1: any, v2: any, v3: any) => ['a', 'b', 'c', 'd', 'e', v1, v2, v3]; - const e2_ff = (v1: any, v2: any, v3: any, v4: any) => ['a', 'b', 'c', 'd', v1, v2, v3, v4]; - const e4_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any) => ['a', 'b', 'c', v1, v2, v3, v4, v5]; - const e6_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any) => ['a', 'b', v1, v2, v3, v4, v5, v6]; - const e8_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, - v7: any) => ['a', v1, v2, v3, v4, v5, v6, v7]; - const e10_ff = - (v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, - v8: any) => [v1, v2, v3, v4, v5, v6, v7, v8]; - - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'my-comp'); - f3Comp = getDirectiveOnNode(0); - ɵɵelementEnd(); - ɵɵelementStart(1, 'my-comp'); - f4Comp = getDirectiveOnNode(1); - ɵɵelementEnd(); - ɵɵelementStart(2, 'my-comp'); - f5Comp = getDirectiveOnNode(2); - ɵɵelementEnd(); - ɵɵelementStart(3, 'my-comp'); - f6Comp = getDirectiveOnNode(3); - ɵɵelementEnd(); - ɵɵelementStart(4, 'my-comp'); - f7Comp = getDirectiveOnNode(4); - ɵɵelementEnd(); - ɵɵelementStart(5, 'my-comp'); - f8Comp = getDirectiveOnNode(5); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'names', ɵɵbind(ɵɵpureFunction3(6, e0_ff, c[5], c[6], c[7]))); - ɵɵelementProperty(1, 'names', ɵɵbind(ɵɵpureFunction4(10, e2_ff, c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 2, 'names', ɵɵbind(ɵɵpureFunction5(15, e4_ff, c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 3, 'names', ɵɵbind(ɵɵpureFunction6(21, e6_ff, c[2], c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 4, 'names', - ɵɵbind(ɵɵpureFunction7(28, e8_ff, c[1], c[2], c[3], c[4], c[5], c[6], c[7]))); - ɵɵelementProperty( - 5, 'names', - ɵɵbind(ɵɵpureFunction8(36, e10_ff, c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]))); - } - } - - renderToHtml(Template, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f7Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - expect(f8Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); - - renderToHtml( - Template, ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1', 'i1'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h1']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h1']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f7Comp !.names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - expect(f8Comp !.names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1']); - - renderToHtml( - Template, ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2', 'i1'], 6, 45, directives); - expect(f3Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e', 'f1', 'g1', 'h2']); - expect(f4Comp !.names).toEqual(['a', 'b', 'c', 'd', 'e1', 'f1', 'g1', 'h2']); - expect(f5Comp !.names).toEqual(['a', 'b', 'c', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f6Comp !.names).toEqual(['a', 'b', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f7Comp !.names).toEqual(['a', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - expect(f8Comp !.names).toEqual(['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h2']); - }); - - it('should work with pureFunctionV for 9+ bindings', () => { - const e0_ff = - (v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, - v8: any) => ['start', v0, v1, v2, v3, v4, v5, v6, v7, v8, 'end']; - const e0_ff_1 = (v: any) => `modified_${v}`; - - /** - * - * - */ - function Template(rf: RenderFlags, c: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'my-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'names', ɵɵbind(ɵɵpureFunctionV(3, e0_ff, [ - c[0], c[1], c[2], c[3], ɵɵpureFunction1(1, e0_ff_1, c[4]), c[5], c[6], c[7], c[8] - ]))); - } - } - - renderToHtml(Template, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' - ]); - - renderToHtml(Template, ['a1', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a1', 'b', 'c', 'd', 'modified_e', 'f', 'g', 'h', 'i', 'end' - ]); - - renderToHtml(Template, ['a1', 'b', 'c', 'd', 'e5', 'f', 'g', 'h', 'i'], 1, 13, directives); - expect(myComp !.names).toEqual([ - 'start', 'a1', 'b', 'c', 'd', 'modified_e5', 'f', 'g', 'h', 'i', 'end' - ]); - }); - -}); describe('object literals', () => { let objectComp: ObjectComp; @@ -393,114 +25,15 @@ describe('object literals', () => { factory: function ObjectComp_Factory() { return objectComp = new ObjectComp(); }, consts: 0, vars: 1, - template: function ObjectComp_Template(rf: RenderFlags, ctx: ObjectComp) {}, + template: function ObjectComp_Template() {}, inputs: {config: 'config'} }); } const defs = [ObjectComp]; - it('should support an object literal', () => { - const e0_ff = (v: any) => { return {duration: 500, animation: v}; }; - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'object-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'config', ɵɵbind(ɵɵpureFunction1(1, e0_ff, ctx.name))); - } - }, 1, 3, defs); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'slide'; - fixture.update(); - const firstObj = objectComp !.config; - expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'}); - - fixture.update(); - expect(objectComp !.config).toEqual({duration: 500, animation: 'slide'}); - expect(firstObj).toBe(objectComp !.config); - - fixture.component.name = 'tap'; - fixture.update(); - expect(objectComp !.config).toEqual({duration: 500, animation: 'tap'}); - - // Identity must change if binding changes - expect(firstObj).not.toBe(objectComp !.config); - }); - - it('should support expressions nested deeply in object/array literals', () => { - const e0_ff = (v1: any, v2: any) => { return {animation: v1, actions: v2}; }; - const e0_ff_1 = (v: any) => [{opacity: 0, duration: 0}, v]; - const e0_ff_2 = (v: any) => { return {opacity: 1, duration: v}; }; - - /** - * - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'object-comp'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty( - 0, 'config', - ɵɵbind(ɵɵpureFunction2( - 5, e0_ff, ctx.name, - ɵɵpureFunction1(3, e0_ff_1, ɵɵpureFunction1(1, e0_ff_2, ctx.duration))))); - } - }, 1, 8, defs); - - const fixture = new ComponentFixture(App); - fixture.component.name = 'slide'; - fixture.component.duration = 100; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] - }); - const firstConfig = objectComp !.config; - - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] - }); - expect(objectComp !.config).toBe(firstConfig); - - fixture.component.duration = 50; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'slide', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] - }); - expect(objectComp !.config).not.toBe(firstConfig); - - fixture.component.name = 'tap'; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'tap', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] - }); - - fixture.component.name = 'drag'; - fixture.component.duration = 500; - fixture.update(); - expect(objectComp !.config).toEqual({ - animation: 'drag', - actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 500}] - }); - - // The property should not be set if the exp value is the same, so artificially - // setting the property to ensure it's not overwritten. - objectComp !.config = ['should not be overwritten']; - fixture.update(); - expect(objectComp !.config).toEqual(['should not be overwritten']); - }); - + // NOTE: This test cannot be ported to acceptance tests with TestBed because + // the syntax is still unsupported. it('should support multiple view instances with multiple bindings', () => { let objectComps: ObjectComp[] = []; diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts index 7d730c8e3a..935bca6a50 100644 --- a/packages/core/test/render3/query_spec.ts +++ b/packages/core/test/render3/query_spec.ts @@ -6,19 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; import {ElementRef, QueryList, TemplateRef, ViewContainerRef} from '@angular/core'; import {EventEmitter} from '../..'; import {AttributeMarker, detectChanges, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵload, ɵɵreference, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; +import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵload, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {query, ɵɵcontentQuery, ɵɵloadContentQuery, ɵɵloadViewQuery, ɵɵqueryRefresh, ɵɵviewQuery} from '../../src/render3/query'; +import {ɵɵcontentQuery, ɵɵloadContentQuery, ɵɵloadViewQuery, ɵɵqueryRefresh, ɵɵviewQuery} from '../../src/render3/query'; import {getLView} from '../../src/render3/state'; import {getNativeByIndex} from '../../src/render3/util/view_utils'; import {ɵɵtemplateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound'; -import {NgForOf, NgIf, NgTemplateOutlet} from './common_with_def'; import {ComponentFixture, TemplateFixture, createComponent, createDirective, getDirectiveOnNode, renderComponent} from './render_util'; @@ -1348,412 +1346,6 @@ describe('query', () => { describe('view boundaries', () => { - describe('ViewContainerRef', () => { - - let directiveInstances: ViewContainerManipulatorDirective[] = []; - - class ViewContainerManipulatorDirective { - static ngDirectiveDef = ɵɵdefineDirective({ - type: ViewContainerManipulatorDirective, - selectors: [['', 'vc', '']], - factory: () => { - const directiveInstance = - new ViewContainerManipulatorDirective(ɵɵdirectiveInject(ViewContainerRef as any)); - directiveInstances.push(directiveInstance); - return directiveInstance; - } - }); - - constructor(private _vcRef: ViewContainerRef) {} - - insertTpl(tpl: TemplateRef<{}>, ctx: {}, idx?: number) { - this._vcRef.createEmbeddedView(tpl, ctx, idx); - } - - remove(index?: number) { this._vcRef.remove(index); } - } - - beforeEach(() => { directiveInstances = []; }); - - it('should report results in views inserted / removed by ngIf', () => { - - function Cmpt_Template_1(rf: RenderFlags, ctx1: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - } - - /** - * - *
        - *
        - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, Cmpt_Template_1, 2, 0, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.value)); - } - }, - 2, 1, [NgIf], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const fixture = new ComponentFixture(Cmpt); - const qList = fixture.component.query; - expect(qList.length).toBe(0); - - fixture.component.value = true; - fixture.update(); - expect(qList.length).toBe(1); - - fixture.component.value = false; - fixture.update(); - expect(qList.length).toBe(0); - }); - - it('should report results in views inserted / removed by ngFor', () => { - - function Cmpt_Template_1(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - if (rf1 & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind(row.$implicit)); - } - } - - /** - * - *
        - *
        - * class Cmpt { - * @ViewChildren('foo') query; - * } - */ - class Cmpt { - // TODO(issue/24571): remove '!'. - value !: string[]; - query: any; - static ngComponentDef = ɵɵdefineComponent({ - type: Cmpt, - factory: () => new Cmpt(), - selectors: [['my-app']], - consts: 2, - vars: 1, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, Cmpt_Template_1, 2, 1, 'ng-template', ['ngForOf', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(ctx.value)); - } - }, - viewQuery: function(rf: RenderFlags, ctx: Cmpt) { - let tmp: any; - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(Cmpt); - const qList = fixture.component.query; - expect(qList.length).toBe(0); - - fixture.component.value = ['a', 'b', 'c']; - fixture.update(); - expect(qList.length).toBe(3); - - fixture.component.value.splice(1, 1); // remove "b" - fixture.update(); - expect(qList.length).toBe(2); - - // make sure that a proper element was removed from query results - expect(qList.first.nativeElement.id).toBe('a'); - expect(qList.last.nativeElement.id).toBe('c'); - - }); - - // https://stackblitz.com/edit/angular-rrmmuf?file=src/app/app.component.ts - it('should report results when different instances of TemplateRef are inserted into one ViewContainerRefs', - () => { - let tpl1: TemplateRef<{}>; - let tpl2: TemplateRef<{}>; - - function Cmpt_Template_1(rf: RenderFlags, ctx: {idx: number}) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind('foo1_' + ctx.idx)); - } - } - - function Cmpt_Template_5(rf: RenderFlags, ctx: {idx: number}) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind('foo2_' + ctx.idx)); - } - } - - /** - * - *
        - *
        - * - *
        - * - * - *
        - *
        - * - * - */ - const Cmpt = createComponent( - 'cmpt', - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, Cmpt_Template_1, 2, 1, 'ng-template', null, ['tpl1', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'div', ['id', 'middle'], ['foo', '']); - ɵɵtemplate( - 4, Cmpt_Template_5, 2, 1, 'ng-template', null, ['tpl2', ''], - ɵɵtemplateRefExtractor); - ɵɵtemplate(6, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'vc']); - } - - if (rf & RenderFlags.Update) { - tpl1 = ɵɵreference(1); - tpl2 = ɵɵreference(5); - } - - }, - 8, 0, [ViewContainerManipulatorDirective], [], - function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (ctx.query = tmp as QueryList); - } - }); - - const fixture = new ComponentFixture(Cmpt); - const qList = fixture.component.query; - - expect(qList.length).toBe(1); - expect(qList.first.nativeElement.getAttribute('id')).toBe('middle'); - - directiveInstances[0].insertTpl(tpl1 !, {idx: 0}, 0); - directiveInstances[0].insertTpl(tpl2 !, {idx: 1}, 1); - fixture.update(); - expect(qList.length).toBe(3); - let qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); - expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); - - directiveInstances[0].insertTpl(tpl1 !, {idx: 1}, 1); - fixture.update(); - expect(qList.length).toBe(4); - qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo1_1'); - expect(qListArr[2].nativeElement.getAttribute('id')).toBe('middle'); - expect(qListArr[3].nativeElement.getAttribute('id')).toBe('foo2_1'); - - directiveInstances[0].remove(1); - fixture.update(); - expect(qList.length).toBe(3); - qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); - expect(qListArr[2].nativeElement.getAttribute('id')).toBe('foo2_1'); - - directiveInstances[0].remove(1); - fixture.update(); - expect(qList.length).toBe(2); - qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('middle'); - }); - - // https://stackblitz.com/edit/angular-7vvo9j?file=src%2Fapp%2Fapp.component.ts - // https://stackblitz.com/edit/angular-xzwp6n - it('should report results when the same TemplateRef is inserted into different ViewContainerRefs', - () => { - let tpl: TemplateRef<{}>; - - function Cmpt_Template_1(rf: RenderFlags, ctx: {idx: number, container_idx: number}) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'id', ɵɵbind('foo_' + ctx.container_idx + '_' + ctx.idx)); - } - } - - /** - * - *
        - *
        - * - * - * - */ - class Cmpt { - query: any; - static ngComponentDef = ɵɵdefineComponent({ - type: Cmpt, - factory: () => new Cmpt(), - selectors: [['my-app']], - consts: 4, - vars: 0, - template: function(rf: RenderFlags, ctx: any) { - let tmp: any; - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, Cmpt_Template_1, 2, 1, 'ng-template', [], ['tpl', ''], - ɵɵtemplateRefExtractor); - ɵɵtemplate(2, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'vc']); - ɵɵtemplate(3, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'vc']); - } - - if (rf & RenderFlags.Update) { - tpl = ɵɵreference(1); - } - - }, - viewQuery: (rf: RenderFlags, cmpt: Cmpt) => { - let tmp: any; - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (cmpt.query = tmp as QueryList); - } - }, - directives: () => [ViewContainerManipulatorDirective], - }); - } - const fixture = new ComponentFixture(Cmpt); - const qList = fixture.component.query; - - expect(qList.length).toBe(0); - - directiveInstances[0].insertTpl(tpl !, {idx: 0, container_idx: 0}, 0); - directiveInstances[1].insertTpl(tpl !, {idx: 0, container_idx: 1}, 0); - fixture.update(); - expect(qList.length).toBe(2); - let qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); - expect(qListArr[1].nativeElement.getAttribute('id')).toBe('foo_0_0'); - - directiveInstances[0].remove(); - fixture.update(); - expect(qList.length).toBe(1); - qListArr = qList.toArray(); - expect(qListArr[0].nativeElement.getAttribute('id')).toBe('foo_1_0'); - - directiveInstances[1].remove(); - fixture.update(); - expect(qList.length).toBe(0); - }); - - // https://stackblitz.com/edit/angular-wpd6gv?file=src%2Fapp%2Fapp.component.ts - it('should report results from views inserted in a lifecycle hook', () => { - - function MyApp_Template_1(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'span', ['id', 'from_tpl'], ['foo', '']); - } - } - - class MyApp { - show = false; - query: any; - static ngComponentDef = ɵɵdefineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 4, - vars: 1, - /** - * - * - */ - template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyApp_Template_1, 2, 0, 'ng-template', undefined, ['tpl', ''], - ɵɵtemplateRefExtractor); - ɵɵtemplate( - 2, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngTemplateOutlet']); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'ngTemplateOutlet', ɵɵbind(myApp.show ? tplRef : null)); - } - }, - directives: () => [NgTemplateOutlet], - viewQuery: (rf: RenderFlags, myApp: MyApp) => { - let tmp: any; - if (rf & RenderFlags.Create) { - ɵɵviewQuery(['foo'], true, null); - } - if (rf & RenderFlags.Update) { - ɵɵqueryRefresh(tmp = ɵɵloadViewQuery>()) && - (myApp.query = tmp as QueryList); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - const qList = fixture.component.query; - - expect(qList.length).toBe(0); - - fixture.component.show = true; - fixture.update(); - expect(qList.length).toBe(1); - expect(qList.first.nativeElement.id).toBe('from_tpl'); - - fixture.component.show = false; - fixture.update(); - expect(qList.length).toBe(0); - }); - - }); - describe('JS blocks', () => { it('should report results in embedded views', () => { @@ -2235,12 +1827,8 @@ describe('query', () => { describe('content', () => { let withContentInstance: WithContentDirective|null; - let shallowCompInstance: ShallowComp|null; - beforeEach(() => { - withContentInstance = null; - shallowCompInstance = null; - }); + beforeEach(() => { withContentInstance = null; }); class WithContentDirective { // @ContentChildren('foo') @@ -2270,29 +1858,6 @@ describe('query', () => { }); } - class ShallowComp { - // @ContentChildren('foo', {descendants: false}) - foos !: QueryList; - - static ngComponentDef = ɵɵdefineComponent({ - type: ShallowComp, - selectors: [['shallow-comp']], - factory: () => shallowCompInstance = new ShallowComp(), - template: function(rf: RenderFlags, ctx: any) {}, - consts: 0, - vars: 0, - contentQueries: (rf: RenderFlags, ctx: any, dirIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵcontentQuery(dirIndex, ['foo'], false, null); - } - if (rf & RenderFlags.Update) { - let tmp: any; - ɵɵqueryRefresh(tmp = ɵɵloadContentQuery()) && (ctx.foos = tmp); - } - } - }); - } - it('should support content queries for directives', () => { /** *
        @@ -2383,43 +1948,6 @@ describe('query', () => { .toBe(0, `Expected content query not to match
        .`); }); - it('should match shallow content queries in views inserted / removed by ngIf', () => { - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', null, ['foo', '']); - } - } - - /** - * - *
        - *
        - */ - const AppComponent = createComponent('app-component', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'shallow-comp'); - { ɵɵtemplate(1, IfTemplate, 2, 0, 'div', [AttributeMarker.Template, 'ngIf', '']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngIf', ɵɵbind(ctx.showing)); - } - }, 2, 1, [ShallowComp, NgIf]); - - const fixture = new ComponentFixture(AppComponent); - const qList = shallowCompInstance !.foos; - expect(qList.length).toBe(0); - - fixture.component.showing = true; - fixture.update(); - expect(qList.length).toBe(1); - - fixture.component.showing = false; - fixture.update(); - expect(qList.length).toBe(0); - }); - - // https://stackblitz.com/edit/angular-wlenwd?file=src%2Fapp%2Fapp.component.ts it('should support view and content queries matching the same element', () => { /** diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 63069703cc..da2ff926bc 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -12,8 +12,9 @@ import {ElementRef} from '@angular/core/src/linker/element_ref'; import {TemplateRef} from '@angular/core/src/linker/template_ref'; import {ViewContainerRef} from '@angular/core/src/linker/view_container_ref'; import {Renderer2} from '@angular/core/src/render/api'; -import {renderTemplate} from '@angular/core/src/render3/instructions/shared'; -import {getLView} from '@angular/core/src/render3/state'; +import {createLView, createTView, getOrCreateTNode, getOrCreateTView, renderComponentOrTemplate} from '@angular/core/src/render3/instructions/shared'; +import {TNodeType} from '@angular/core/src/render3/interfaces/node'; +import {enterView, getLView, resetComponentState} from '@angular/core/src/render3/state'; import {stringifyElement} from '@angular/platform-browser/testing/src/browser_util'; import {SWITCH_CHANGE_DETECTOR_REF_FACTORY__POST_R3__ as R3_CHANGE_DETECTOR_REF_FACTORY} from '../../src/change_detection/change_detector_ref'; @@ -27,11 +28,11 @@ import {CreateComponentOptions} from '../../src/render3/component'; import {getDirectivesAtNodeIndex, getLContext, isComponentInstance} from '../../src/render3/context_discovery'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; import {NG_ELEMENT_ID} from '../../src/render3/fields'; -import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, RenderFlags, renderComponent as _renderComponent, tick, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; -import {DirectiveDefList, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; +import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, RenderFlags, renderComponent as _renderComponent, tick, ɵɵProvidersFeature, ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index'; +import {DirectiveDefList, DirectiveDefListOrFactory, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeDefList, PipeDefListOrFactory, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {PlayerHandler} from '../../src/render3/interfaces/player'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, RendererStyleFlags3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; -import {HEADER_OFFSET, LView} from '../../src/render3/interfaces/view'; +import {HEADER_OFFSET, LView, LViewFlags, TVIEW, T_HOST} from '../../src/render3/interfaces/view'; import {destroyLView} from '../../src/render3/node_manipulation'; import {getRootView} from '../../src/render3/util/view_traversal_utils'; import {Sanitizer} from '../../src/sanitization/security'; @@ -230,6 +231,57 @@ export function resetDOM() { // TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc) } + +/** + * + * @param hostNode Existing node to render into. + * @param templateFn Template function with the instructions. + * @param consts The number of nodes, local refs, and pipes in this template + * @param context to pass into the template. + * @param providedRendererFactory renderer factory to use + * @param host The host element node to use + * @param directives Directive defs that should be used for matching + * @param pipes Pipe defs that should be used for matching + */ +export function renderTemplate( + hostNode: RElement, templateFn: ComponentTemplate, consts: number, vars: number, context: T, + providedRendererFactory: RendererFactory3, componentView: LView | null, + directives?: DirectiveDefListOrFactory | null, pipes?: PipeDefListOrFactory | null, + sanitizer?: Sanitizer | null): LView { + if (componentView === null) { + resetComponentState(); + const renderer = providedRendererFactory.createRenderer(null, null); + + // We need to create a root view so it's possible to look up the host element through its index + const tView = createTView(-1, null, 1, 0, null, null, null, null); + const hostLView = createLView( + null, tView, {}, LViewFlags.CheckAlways | LViewFlags.IsRoot, null, null, + providedRendererFactory, renderer); + enterView(hostLView, null); // SUSPECT! why do we need to enter the View? + + const def: ComponentDef = ɵɵdefineComponent({ + factory: () => null, + selectors: [], + type: Object, + template: templateFn, + consts: consts, + vars: vars, + }); + def.directiveDefs = directives || null; + def.pipeDefs = pipes || null; + + const componentTView = getOrCreateTView(def); + const hostTNode = getOrCreateTNode(tView, hostLView[T_HOST], 0, TNodeType.Element, null, null); + hostLView[hostTNode.index] = hostNode; + componentView = createLView( + hostLView, componentTView, context, LViewFlags.CheckAlways, hostNode, hostTNode, + providedRendererFactory, renderer, sanitizer); + } + renderComponentOrTemplate(componentView, context, templateFn); + return componentView; +} + + /** * @deprecated use `TemplateFixture` or `ComponentFixture` */ diff --git a/packages/core/test/render3/renderer_factory_spec.ts b/packages/core/test/render3/renderer_factory_spec.ts index 4d3952b83c..1e7473bdbd 100644 --- a/packages/core/test/render3/renderer_factory_spec.ts +++ b/packages/core/test/render3/renderer_factory_spec.ts @@ -6,16 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationEvent} from '@angular/animations'; -import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; - import {RendererType2, ViewEncapsulation} from '../../src/core'; import {ɵɵdefineComponent} from '../../src/render3/index'; -import {tick, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all'; +import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtext} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2'; -import {TemplateFixture, containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util'; +import {getRendererFactory2} from './imported_renderer2'; +import {TemplateFixture, document, renderToHtml} from './render_util'; describe('renderer factory lifecycle', () => { let logs: string[] = []; @@ -87,21 +84,6 @@ describe('renderer factory lifecycle', () => { beforeEach(() => { logs = []; }); - it('should work with a component', () => { - const component = renderComponent(SomeComponent, {rendererFactory}); - expect(logs).toEqual( - ['create', 'create', 'begin', 'component create', 'component update', 'end']); - - logs = []; - tick(component); - expect(logs).toEqual(['begin', 'component update', 'end']); - }); - - it('should work with a component which throws', () => { - expect(() => renderComponent(SomeComponentWhichThrows, {rendererFactory})).toThrow(); - expect(logs).toEqual(['create', 'create', 'begin', 'end']); - }); - it('should work with a template', () => { renderToHtml(Template, {}, 1, 0, null, null, rendererFactory); expect(logs).toEqual(['create', 'function create', 'function update']); @@ -125,108 +107,6 @@ describe('renderer factory lifecycle', () => { }); -describe('animation renderer factory', () => { - let eventLogs: string[] = []; - function getLog(): MockAnimationPlayer[] { - return MockAnimationDriver.log as MockAnimationPlayer[]; - } - - function resetLog() { MockAnimationDriver.log = []; } - - beforeEach(() => { - eventLogs = []; - resetLog(); - }); - - class SomeComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-component']], - consts: 1, - vars: 0, - template: function(rf: RenderFlags, ctx: SomeComponent) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - }, - factory: () => new SomeComponent - }); - } - - class SomeComponentWithAnimation { - // TODO(issue/24571): remove '!'. - exp !: string; - callback(event: AnimationEvent) { - eventLogs.push(`${event.fromState ? event.fromState : event.toState} - ${event.phaseName}`); - } - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponentWithAnimation, - selectors: [['some-component']], - consts: 2, - vars: 1, - template: function(rf: RenderFlags, ctx: SomeComponentWithAnimation) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵlistener('@myAnimation.start', ctx.callback.bind(ctx)); - ɵɵlistener('@myAnimation.done', ctx.callback.bind(ctx)); - ɵɵtext(1, 'foo'); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, '@myAnimation', ɵɵbind(ctx.exp)); - } - }, - factory: () => new SomeComponentWithAnimation, - encapsulation: ViewEncapsulation.None, - styles: [], - data: { - animation: [{ - type: 7, - name: 'myAnimation', - definitions: [{ - type: 1, - expr: '* => on', - animation: - [{type: 4, styles: {type: 6, styles: {opacity: 1}, offset: null}, timings: 10}], - options: null - }], - options: {} - }] - }, - }); - } - - it('should work with components without animations', () => { - renderComponent(SomeComponent, {rendererFactory: getAnimationRendererFactory2(document)}); - expect(toHtml(containerEl)).toEqual('foo'); - }); - - isBrowser && it('should work with animated components', (done) => { - const rendererFactory = getAnimationRendererFactory2(document); - const component = renderComponent(SomeComponentWithAnimation, {rendererFactory}); - expect(toHtml(containerEl)) - .toMatch(/
        foo<\/div>/); - - component.exp = 'on'; - tick(component); - - const [player] = getLog(); - expect(player.keyframes).toEqual([ - {opacity: '*', offset: 0}, - {opacity: 1, offset: 1}, - ]); - player.finish(); - - rendererFactory.whenRenderingDone !().then(() => { - expect(eventLogs).toEqual(['void - start', 'void - done', 'on - start', 'on - done']); - done(); - }); - }); -}); - describe('Renderer2 destruction hooks', () => { const rendererFactory = getRendererFactory2(document); diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index 3dfecb239f..4f6120edd2 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -23,7 +23,7 @@ import {registerHostDirective} from '../../../src/render3/styling/host_instructi import {BoundPlayerFactory, bindPlayerFactory} from '../../../src/render3/styling/player_factory'; import {allocStylingContext, createEmptyStylingContext} from '../../../src/render3/styling/util'; import {ɵɵdefaultStyleSanitizer} from '../../../src/sanitization/sanitization'; -import {StyleSanitizeFn} from '../../../src/sanitization/style_sanitizer'; +import {StyleSanitizeFn, StyleSanitizeMode} from '../../../src/sanitization/style_sanitizer'; import {ComponentFixture, renderToHtml} from '../render_util'; import {MockPlayer} from './mock_player'; @@ -2991,11 +2991,16 @@ describe('style and class based bindings', () => { }); it('should sanitize styles before they are passed into the player', () => { - const sanitizer = (function(prop: string, value?: string): string | boolean { - if (value === undefined) { - return prop === 'width' || prop === 'height'; + const sanitizer = (function(prop: string, value: string, mode: StyleSanitizeMode): any { + let allow = true; + if (mode & StyleSanitizeMode.ValidateProperty) { + allow = prop === 'width' || prop === 'height'; + } + + if (mode & StyleSanitizeMode.SanitizeOnly) { + return allow ? `${value}-safe!` : value; } else { - return `${value}-safe!`; + return allow; } }) as StyleSanitizeFn; diff --git a/packages/core/test/render3/styling_next/map_based_bindings_spec.ts b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts new file mode 100644 index 0000000000..a6ebd1e439 --- /dev/null +++ b/packages/core/test/render3/styling_next/map_based_bindings_spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. 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 {normalizeIntoStylingMap as createMap} from '../../../src/render3/styling_next/map_based_bindings'; + +describe('map-based bindings', () => { + describe('LStylingMap construction', () => { + it('should create a new LStylingMap instance from a given value', () => { + createAndAssertValues(null, []); + createAndAssertValues(undefined, []); + createAndAssertValues({}, []); + createAndAssertValues({foo: 'bar'}, ['foo', 'bar']); + createAndAssertValues({bar: null}, ['bar', null]); + createAndAssertValues('', []); + createAndAssertValues('abc xyz', ['abc', true, 'xyz', true]); + createAndAssertValues([], []); + }); + + it('should list each entry in the context in alphabetical order', () => { + const value1 = {width: '200px', color: 'red', zIndex: -1}; + const map1 = createMap(null, value1); + expect(map1).toEqual([value1, 'color', 'red', 'width', '200px', 'zIndex', -1]); + + const value2 = 'yes no maybe'; + const map2 = createMap(null, value2); + expect(map2).toEqual([value2, 'maybe', true, 'no', true, 'yes', true]); + }); + + it('should patch an existing LStylingMap entry with new values and retain the alphabetical order', + () => { + const value1 = {color: 'red'}; + const map1 = createMap(null, value1); + expect(map1).toEqual([value1, 'color', 'red']); + + const value2 = {backgroundColor: 'red', color: 'blue', opacity: '0.5'}; + const map2 = createMap(map1, value2); + expect(map1).toBe(map2); + expect(map1).toEqual( + [value2, 'backgroundColor', 'red', 'color', 'blue', 'opacity', '0.5']); + + const value3 = 'myClass'; + const map3 = createMap(null, value3); + expect(map3).toEqual([value3, 'myClass', true]); + + const value4 = 'yourClass everyonesClass myClass'; + const map4 = createMap(map3, value4); + expect(map3).toBe(map4); + expect(map4).toEqual([value4, 'everyonesClass', true, 'myClass', true, 'yourClass', true]); + }); + + it('should nullify old values that are not apart of the new set of values', () => { + const value1 = {color: 'red', fontSize: '20px'}; + const map1 = createMap(null, value1); + expect(map1).toEqual([value1, 'color', 'red', 'fontSize', '20px']); + + const value2 = {color: 'blue', borderColor: 'purple', opacity: '0.5'}; + const map2 = createMap(map1, value2); + expect(map2).toEqual( + [value2, 'borderColor', 'purple', 'color', 'blue', 'fontSize', null, 'opacity', '0.5']); + + const value3 = 'orange'; + const map3 = createMap(null, value3); + expect(map3).toEqual([value3, 'orange', true]); + + const value4 = 'apple banana'; + const map4 = createMap(map3, value4); + expect(map4).toEqual([value4, 'apple', true, 'banana', true, 'orange', null]); + }); + }); +}); + +function createAndAssertValues(newValue: any, entries: any[]) { + const result = createMap(null, newValue); + expect(result).toEqual([newValue || null, ...entries]); +} diff --git a/packages/core/test/render3/styling_next/styling_context_spec.ts b/packages/core/test/render3/styling_next/styling_context_spec.ts new file mode 100644 index 0000000000..e84137ba6b --- /dev/null +++ b/packages/core/test/render3/styling_next/styling_context_spec.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google Inc. 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 {DEFAULT_GUARD_MASK_VALUE, registerBinding} from '@angular/core/src/render3/styling_next/bindings'; +import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug'; +import {allocTStylingContext} from '../../../src/render3/styling_next/util'; + +describe('styling context', () => { + it('should register a series of entries into the context', () => { + const debug = makeContextWithDebug(); + const context = debug.context; + expect(debug.entries).toEqual({}); + + registerBinding(context, 1, 'width', '100px'); + expect(debug.entries['width']).toEqual({ + prop: 'width', + valuesCount: 1, + sanitizationRequired: false, + guardMask: buildGuardMask(), + defaultValue: '100px', + sources: ['100px'], + }); + + registerBinding(context, 2, 'width', 20); + expect(debug.entries['width']).toEqual({ + prop: 'width', + sanitizationRequired: false, + valuesCount: 2, + guardMask: buildGuardMask(2), + defaultValue: '100px', + sources: [20, '100px'], + }); + + registerBinding(context, 3, 'height', 10); + registerBinding(context, 4, 'height', 15); + expect(debug.entries['height']).toEqual({ + prop: 'height', + valuesCount: 3, + sanitizationRequired: false, + guardMask: buildGuardMask(3, 4), + defaultValue: null, + sources: [10, 15, null], + }); + }); + + it('should overwrite a default value for an entry only if it is non-null', () => { + const debug = makeContextWithDebug(); + const context = debug.context; + + registerBinding(context, 1, 'width', null); + const x = debug.entries['width']; + expect(debug.entries['width']).toEqual({ + prop: 'width', + valuesCount: 1, + sanitizationRequired: false, + guardMask: buildGuardMask(), + defaultValue: null, + sources: [null] + }); + + registerBinding(context, 1, 'width', '100px'); + expect(debug.entries['width']).toEqual({ + prop: 'width', + valuesCount: 1, + sanitizationRequired: false, + guardMask: buildGuardMask(), + defaultValue: '100px', + sources: ['100px'] + }); + + registerBinding(context, 1, 'width', '200px'); + expect(debug.entries['width']).toEqual({ + prop: 'width', + valuesCount: 1, + sanitizationRequired: false, + guardMask: buildGuardMask(), + defaultValue: '100px', + sources: ['100px'] + }); + }); +}); + +function makeContextWithDebug() { + const ctx = allocTStylingContext(); + return attachStylingDebugObject(ctx); +} + +function buildGuardMask(...bindingIndices: number[]) { + let mask = DEFAULT_GUARD_MASK_VALUE; + for (let i = 0; i < bindingIndices.length; i++) { + mask |= 1 << bindingIndices[i]; + } + return mask; +} diff --git a/packages/core/test/render3/styling_next/styling_debug_spec.ts b/packages/core/test/render3/styling_next/styling_debug_spec.ts new file mode 100644 index 0000000000..138def5d09 --- /dev/null +++ b/packages/core/test/render3/styling_next/styling_debug_spec.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright Google Inc. 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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings'; +import {NodeStylingDebug, attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug'; +import {allocTStylingContext} from '@angular/core/src/render3/styling_next/util'; + +describe('styling debugging tools', () => { + describe('NodeStylingDebug', () => { + it('should list out each of the values in the context paired together with the provided data', + () => { + const debug = makeContextWithDebug(); + const context = debug.context; + const data: any[] = []; + const d = new NodeStylingDebug(context, data); + + registerBinding(context, 0, 'width', null); + expect(d.summary).toEqual({ + width: { + prop: 'width', + value: null, + bindingIndex: null, + }, + }); + + registerBinding(context, 0, 'width', '100px'); + expect(d.summary).toEqual({ + width: { + prop: 'width', + value: '100px', + bindingIndex: null, + }, + }); + + const someBindingIndex1 = 1; + data[someBindingIndex1] = '200px'; + + registerBinding(context, 0, 'width', someBindingIndex1); + expect(d.summary).toEqual({ + width: { + prop: 'width', + value: '200px', + bindingIndex: someBindingIndex1, + }, + }); + + const someBindingIndex2 = 2; + data[someBindingIndex2] = '500px'; + + registerBinding(context, 0, 'width', someBindingIndex2); + expect(d.summary).toEqual({ + width: { + prop: 'width', + value: '200px', + bindingIndex: someBindingIndex1, + }, + }); + }); + }); +}); + +function makeContextWithDebug() { + const ctx = allocTStylingContext(); + return attachStylingDebugObject(ctx); +} diff --git a/packages/core/test/render3/template_ref_spec.ts b/packages/core/test/render3/template_ref_spec.ts deleted file mode 100644 index 36b2b70b93..0000000000 --- a/packages/core/test/render3/template_ref_spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * @license - * Copyright Google Inc. 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 {TemplateRef} from '@angular/core'; - -import {AttributeMarker, RenderFlags, ɵɵdefineDirective} from '../../src/render3/index'; -import {ɵɵbind, ɵɵdirectiveInject, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementProperty, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; - -import {NgIf} from './common_with_def'; -import {ComponentFixture, createComponent, getDirectiveOnNode} from './render_util'; - -describe('TemplateRef', () => { - - describe('rootNodes', () => { - - class DirectiveWithTplRef { - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveWithTplRef, - selectors: [['', 'tplRef', '']], - factory: () => new DirectiveWithTplRef(ɵɵdirectiveInject(TemplateRef as any)) - }); - - // injecting a ViewContainerRef to create a dynamic container in which embedded views will be - // created - constructor(public tplRef: TemplateRef<{}>) {} - } - - it('should return root render nodes for an embedded view instance', () => { - let directiveWithTplRef: DirectiveWithTplRef; - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div'); - ɵɵtext(1, 'some text'); - ɵɵelement(2, 'span'); - } - } - - /* - -
        - some text - -
        - */ - const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, embeddedTemplate, 3, 0, 'ng-template', ['tplRef', '']); - directiveWithTplRef = getDirectiveOnNode(0, 0); - } - }, 1, 0, [DirectiveWithTplRef]); - - - const fixture = new ComponentFixture(AppComponent); - expect(directiveWithTplRef !).toBeDefined(); - - const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); - expect(viewRef.rootNodes.length).toBe(3); - }); - - /** - * This is different as compared to the view engine implementation which returns a comment node - * in this case: - * https://stackblitz.com/edit/angular-uiqry6?file=src/app/app.component.ts - * - * Returning a comment node for a template ref with no nodes is wrong and should be fixed in - * ivy. - */ - it('should return an empty array for embedded view with no nodes', () => { - let directiveWithTplRef: DirectiveWithTplRef; - - /* - - */ - const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, () => {}, 0, 0, 'ng-template', ['tplRef', '']); - directiveWithTplRef = getDirectiveOnNode(0, 0); - } - }, 1, 0, [DirectiveWithTplRef]); - - - const fixture = new ComponentFixture(AppComponent); - expect(directiveWithTplRef !).toBeDefined(); - - const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); - expect(viewRef.rootNodes.length).toBe(0); - }); - - /** - * This is somehow surprising but the current view engine don't descend into containers when - * getting root nodes of an embedded view: - * https://stackblitz.com/edit/angular-z8zev7?file=src/app/app.component.ts - */ - it('should not descend into containers when retrieving root nodes', () => { - let directiveWithTplRef: DirectiveWithTplRef; - - function ngIfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'text'); - } - } - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, ngIfTemplate, 1, 0, 'ng-template', [AttributeMarker.Bindings, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.showing)); - } - } - - /* - text - */ - const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, embeddedTemplate, 1, 1, 'ng-template', ['tplRef', '']); - directiveWithTplRef = getDirectiveOnNode(0, 0); - } - }, 1, 0, [DirectiveWithTplRef, NgIf]); - - - const fixture = new ComponentFixture(AppComponent); - expect(directiveWithTplRef !).toBeDefined(); - - const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); - - // assert that we've got a comment node (only!) corresponding to - expect(viewRef.rootNodes.length).toBe(1); - expect(viewRef.rootNodes[0].nodeType).toBe(8); - }); - - - /** - * Contrary to containers () we _do_ descend into element containers - * ( { - let directiveWithTplRef: DirectiveWithTplRef; - - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0); - { ɵɵtext(1, 'text'); } - ɵɵelementContainerEnd(); - } - } - - /* - text - */ - const AppComponent = createComponent('app-cmp', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, embeddedTemplate, 2, 0, 'ng-template', ['tplRef', '']); - directiveWithTplRef = getDirectiveOnNode(0, 0); - } - }, 1, 0, [DirectiveWithTplRef]); - - - const fixture = new ComponentFixture(AppComponent); - expect(directiveWithTplRef !).toBeDefined(); - - const viewRef = directiveWithTplRef !.tplRef.createEmbeddedView({}); - - expect(viewRef.rootNodes.length).toBe(2); - expect(viewRef.rootNodes[0].nodeType) - .toBe(8); // a comment node (only!) corresponding to - expect(viewRef.rootNodes[1].nodeType).toBe(3); // a text node - }); - }); -}); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 0e1ab26879..a56f0834ae 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -6,23 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, ɵɵdefineInjector, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, ViewRef, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef,} from '../../src/core'; -import {createInjector} from '../../src/di/r3_injector'; +import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, ElementRef, QueryList, TemplateRef, ViewContainerRef, ViewRef,} from '../../src/core'; import {ViewEncapsulation} from '../../src/metadata'; -import {AttributeMarker, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdefinePipe, injectComponentFactoryResolver, ɵɵlistener, ɵɵloadViewQuery, ɵɵNgOnChangesFeature, ɵɵqueryRefresh, ɵɵviewQuery,} from '../../src/render3/index'; +import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵlistener, ɵɵloadViewQuery, ɵɵqueryRefresh, ɵɵviewQuery,} from '../../src/render3/index'; -import {ɵɵallocHostVars, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementHostAttrs, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation1, ɵɵinterpolation3, ɵɵnextContext, ɵɵprojection, ɵɵprojectionDef, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding,} from '../../src/render3/instructions/all'; +import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtemplate, ɵɵtext,} from '../../src/render3/instructions/all'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RElement} from '../../src/render3/interfaces/renderer'; -import {NgModuleFactory} from '../../src/render3/ng_module_ref'; -import {ɵɵpipe, ɵɵpipeBind1} from '../../src/render3/pipe'; import {getLView} from '../../src/render3/state'; import {getNativeByIndex} from '../../src/render3/util/view_utils'; -import {ɵɵtemplateRefExtractor} from '../../src/render3/view_engine_compatibility_prebound'; -import {NgForOf} from '../../test/render3/common_with_def'; - -import {getRendererFactory2} from './imported_renderer2'; -import {ComponentFixture, createComponent, getDirectiveOnNode, TemplateFixture,} from './render_util'; +import {ComponentFixture, createComponent, TemplateFixture,} from './render_util'; const Component: typeof _Component = function(...args: any[]): any { // In test we use @Component for documentation only so it's safe to mock out the implementation. @@ -56,184 +49,8 @@ describe('ViewContainerRef', () => { } describe('API', () => { - /** - * {{name}} - */ - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.name)); - } - } - - function createView(s: string, index?: number): EmbeddedViewRef { - return directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, {name: s}, index); - } - - /** - * {{name}} - *

        - */ - function createTemplate() { - ɵɵtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'p', ['vcref', '']); - } - - function updateTemplate() { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tplRef', ɵɵbind(tplRef)); - } describe('createEmbeddedView (incl. insert)', () => { - it('should work on elements', () => { - /** - * {{name}} - *
        - *
        - */ - function createTemplate() { - ɵɵtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'header', ['vcref', '']); - ɵɵelement(3, 'footer'); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 4, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
        '); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('
        A
        '); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('
        ABC
        '); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('
        YABC
        '); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); - - it('should work on components', () => { - const HeaderComponent = - createComponent('header-cmp', function(rf: RenderFlags, ctx: any) {}); - - /** - * {{name}} - * - *
        - */ - function createTemplate() { - ɵɵtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', [], ['tplRef', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'header-cmp', ['vcref', '']); - ɵɵelement(3, 'footer'); - } - - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 4, 1, [HeaderComponent, DirectiveWithVCRef]); - expect(fixture.html).toEqual('
        '); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('A
        '); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('ABC
        '); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('YABC
        '); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); - - it('should work with multiple instances with vcrefs', () => { - let firstDir: DirectiveWithVCRef; - let secondDir: DirectiveWithVCRef; - - /** - * {{name}} - *
        - *
        - */ - function createTemplate() { - ɵɵtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', null, ['tplRef', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'div', ['vcref', '']); - ɵɵelement(3, 'div', ['vcref', '']); - - // for testing only: - firstDir = getDirectiveOnNode(2); - secondDir = getDirectiveOnNode(3); - } - - function update() { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(3, 'tplRef', ɵɵbind(tplRef)); - } - - const fixture = new TemplateFixture(createTemplate, update, 4, 2, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
        '); - - firstDir !.vcref.createEmbeddedView(firstDir !.tplRef, {name: 'A'}); - secondDir !.vcref.createEmbeddedView(secondDir !.tplRef, {name: 'B'}); - fixture.update(); - expect(fixture.html).toEqual('
        A
        B'); - }); - - it('should work on templates', () => { - /** - * {{name}} - *
        - */ - function createTemplate() { - ɵɵtemplate( - 0, embeddedTemplate, 1, 1, 'ng-template', ['vcref', ''], ['tplRef', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'footer'); - } - - function updateTemplate() { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(0, 'tplRef', ɵɵbind(tplRef)); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
        '); - - createView('A'); - fixture.update(); - expect(fixture.html).toEqual('A
        '); - - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('ABC
        '); - - createView('Y', 0); - fixture.update(); - expect(fixture.html).toEqual('YABC
        '); - - expect(() => { createView('Z', -1); }).toThrow(); - expect(() => { createView('Z', 5); }).toThrow(); - }); it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)', () => { @@ -411,646 +228,11 @@ describe('ViewContainerRef', () => { expect(fixture.html).toEqual('before|AAB|after'); }); - it('should apply directives and pipes of the host view to the TemplateRef', () => { - @Component({selector: 'child', template: `{{name}}`}) - class Child { - // TODO(issue/24571): remove '!'. - name !: string; - - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', cmp.name, '')); - } - }, - inputs: {name: 'name'} - }); - } - - @Pipe({name: 'starPipe'}) - class StarPipe implements PipeTransform { - transform(value: any) { return `**${value}**`; } - - static ngPipeDef = ɵɵdefinePipe({ - name: 'starPipe', - type: StarPipe, - factory: function StarPipe_Factory() { return new StarPipe(); }, - }); - } - - function SomeComponent_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child'); - ɵɵpipe(1, 'starPipe'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'name', ɵɵbind(ɵɵpipeBind1(1, 1, 'C'))); - } - } - - @Component({ - template: ` - - - - - - ` - }) - class SomeComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 6, - vars: 7, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, SomeComponent_Template_0, 2, 3, 'ng-template', [], ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵpipe(2, 'starPipe'); - ɵɵelement(3, 'child', ['vcref', '']); - ɵɵpipe(4, 'starPipe'); - ɵɵelement(5, 'child'); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(3, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(3, 'name', ɵɵbind(ɵɵpipeBind1(2, 3, 'A'))); - ɵɵelementProperty(5, 'name', ɵɵbind(ɵɵpipeBind1(4, 5, 'B'))); - } - }, - directives: [Child, DirectiveWithVCRef], - pipes: [StarPipe] - }); - } - - const fixture = new ComponentFixture(SomeComponent); - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '**A****C****C****B**'); - }); - }); - - describe('insertion points and declaration points', () => { - class InsertionDir { - // @Input() - set tplDir(tpl: TemplateRef|null) { - tpl ? this.vcr.createEmbeddedView(tpl) : this.vcr.clear(); - } - - constructor(public vcr: ViewContainerRef) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - type: InsertionDir, - selectors: [['', 'tplDir', '']], - factory: () => new InsertionDir(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {tplDir: 'tplDir'} - }); - } - - // see running stackblitz example: https://stackblitz.com/edit/angular-w3myy6 - it('should work with a template declared in a different component view from insertion', - () => { - let child: Child|null = null; - - /** - *
        {{ name }}
        - * // template insertion point - */ - class Child { - name = 'Child'; - tpl: TemplateRef|null = null; - - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => child = new Child(), - consts: 2, - vars: 2, - template: function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [AttributeMarker.Bindings, 'tplDir']); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'tplDir', ɵɵbind(ctx.tpl)); - ɵɵtextBinding(1, ɵɵbind(ctx.name)); - } - }, - inputs: {tpl: 'tpl'}, - directives: () => [InsertionDir] - }); - } - - /** - * // template declaration point - * - *
        {{ name }}
        - *
        - * - * <-- template insertion inside - */ - const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, fooTemplate, 2, 1, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - ɵɵelement(2, 'child'); - } - - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tpl', ɵɵbind(tplRef)); - } - - }, 3, 1, [Child]); - - function fooTemplate(rf1: RenderFlags, ctx: any) { - if (rf1 & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf1 & RenderFlags.Update) { - const parent = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵbind(parent.name)); - } - } - - const fixture = new ComponentFixture(Parent); - fixture.component.name = 'Parent'; - fixture.update(); - - // Context should be inherited from the declaration point, not the insertion point, - // so the template should read 'Parent'. - expect(fixture.html).toEqual(`
        Child
        Parent
        `); - - child !.tpl = null; - fixture.update(); - expect(fixture.html).toEqual(`
        Child
        `); - }); - - // see running stackblitz example: https://stackblitz.com/edit/angular-3vplec - it('should work with nested for loops with different declaration / insertion points', () => { - /** - * - * // insertion point for templates (both row and cell) - * - */ - class LoopComp { - name = 'Loop'; - - // @Input() - tpl !: TemplateRef; - - // @Input() - rows !: any[]; - - static ngComponentDef = ɵɵdefineComponent({ - type: LoopComp, - encapsulation: ViewEncapsulation.None, - selectors: [['loop-comp']], - factory: () => new LoopComp(), - consts: 1, - vars: 2, - template: function(rf: RenderFlags, loop: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'ngForOf']); - } - - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngForOf', ɵɵbind(loop.rows)); - ɵɵelementProperty(0, 'ngForTemplate', ɵɵbind(loop.tpl)); - } - }, - inputs: {tpl: 'tpl', rows: 'rows'}, - directives: () => [NgForOf] - }); - } - - /** - * // row declaration point - * - * - * // cell declaration point - * - *
        {{ cell }} - {{ row.value }} - {{ name }}
        - *
        - * - * <-- cell insertion - *
        - * - * <-- row insertion - * - */ - const Parent = createComponent('parent', function(rf: RenderFlags, parent: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, rowTemplate, 3, 2, 'ng-template', null, ['rowTemplate', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'loop-comp'); - } - - if (rf & RenderFlags.Update) { - const rowTemplateRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tpl', ɵɵbind(rowTemplateRef)); - ɵɵelementProperty(2, 'rows', ɵɵbind(parent.rows)); - } - - }, 3, 2, [LoopComp]); - - function rowTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, cellTemplate, 2, 3, 'ng-template', null, ['cellTemplate', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'loop-comp'); - } - - if (rf & RenderFlags.Update) { - const row = ctx.$implicit as any; - const cellTemplateRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tpl', ɵɵbind(cellTemplateRef)); - ɵɵelementProperty(2, 'rows', ɵɵbind(row.data)); - } - } - - function cellTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - - if (rf & RenderFlags.Update) { - const cell = ctx.$implicit as any; - const row = ɵɵnextContext().$implicit as any; - const parent = ɵɵnextContext(); - ɵɵtextBinding(1, ɵɵinterpolation3('', cell, ' - ', row.value, ' - ', parent.name, '')); - } - } - - const fixture = new ComponentFixture(Parent); - fixture.component.name = 'Parent'; - fixture.component.rows = - [{data: ['1', '2'], value: 'one'}, {data: ['3', '4'], value: 'two'}]; - fixture.update(); - - expect(fixture.html) - .toEqual( - '' + - '
        1 - one - Parent
        2 - one - Parent
        ' + - '
        3 - two - Parent
        4 - two - Parent
        ' + - '
        '); - - fixture.component.rows = [{data: ['5', '6'], value: 'three'}, {data: ['7'], value: 'four'}]; - fixture.component.name = 'New name!'; - fixture.update(); - - expect(fixture.html) - .toEqual( - '' + - '
        5 - three - New name!
        6 - three - New name!
        ' + - '
        7 - four - New name!
        ' + - '
        '); - }); - }); - - const rendererFactory = getRendererFactory2(document); - - describe('detach', () => { - it('should detach the right embedded view when an index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - createView('B'); - createView('C'); - const viewD = createView('D'); - createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

        ABCDE'); - - directiveInstance !.vcref.detach(3); - fixture.update(); - expect(fixture.html).toEqual('

        ABCE'); - expect(viewD.destroyed).toBeFalsy(); - - directiveInstance !.vcref.detach(0); - fixture.update(); - expect(fixture.html).toEqual('

        BCE'); - expect(viewA.destroyed).toBeFalsy(); - - expect(() => { directiveInstance !.vcref.detach(-1); }).toThrow(); - expect(() => { directiveInstance !.vcref.detach(42); }).toThrow(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 0}); - }); - - - it('should detach the last embedded view when no index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - createView('A'); - createView('B'); - createView('C'); - createView('D'); - const viewE = createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

        ABCDE'); - - directiveInstance !.vcref.detach(); - fixture.update(); - expect(fixture.html).toEqual('

        ABCD'); - expect(viewE.destroyed).toBeFalsy(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 0}); - }); - }); - - describe('remove', () => { - it('should remove the right embedded view when an index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - createView('B'); - createView('C'); - const viewD = createView('D'); - createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

        ABCDE'); - - directiveInstance !.vcref.remove(3); - fixture.update(); - expect(fixture.html).toEqual('

        ABCE'); - expect(viewD.destroyed).toBeTruthy(); - - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(fixture.html).toEqual('

        BCE'); - expect(viewA.destroyed).toBeTruthy(); - - expect(() => { directiveInstance !.vcref.remove(-1); }).toThrow(); - expect(() => { directiveInstance !.vcref.remove(42); }).toThrow(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 2}); - }); - - it('should remove the last embedded view when no index is specified', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - createView('A'); - createView('B'); - createView('C'); - createView('D'); - const viewE = createView('E'); - fixture.update(); - expect(fixture.html).toEqual('

        ABCDE'); - - directiveInstance !.vcref.remove(); - fixture.update(); - expect(fixture.html).toEqual('

        ABCD'); - expect(viewE.destroyed).toBeTruthy(); - expect(ngDevMode).toHaveProperties({rendererDestroyNode: 1}); - }); - - it('should throw when trying to insert a removed or destroyed view', () => { - const fixture = new TemplateFixture( - createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef], null, null, - rendererFactory); - const viewA = createView('A'); - const viewB = createView('B'); - fixture.update(); - - directiveInstance !.vcref.remove(); - fixture.update(); - expect(() => directiveInstance !.vcref.insert(viewB)).toThrow(); - - viewA.destroy(); - fixture.update(); - expect(() => directiveInstance !.vcref.insert(viewA)).toThrow(); - }); - }); - - describe('length', () => { - it('should return the number of embedded views', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(directiveInstance !.vcref.length).toEqual(0); - - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(3); - - directiveInstance !.vcref.detach(1); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(2); - - directiveInstance !.vcref.clear(); - fixture.update(); - expect(directiveInstance !.vcref.length).toEqual(0); - }); - }); - - describe('get and indexOf', () => { - it('should retrieve a ViewRef from its index, and vice versa', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - - let viewRef = directiveInstance !.vcref.get(0); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(0); - - viewRef = directiveInstance !.vcref.get(1); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(1); - - viewRef = directiveInstance !.vcref.get(2); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(2); - }); - - it('should handle out of bounds cases', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - fixture.update(); - - expect(directiveInstance !.vcref.get(-1)).toBeNull(); - expect(directiveInstance !.vcref.get(42)).toBeNull(); - - const viewRef = directiveInstance !.vcref.get(0); - directiveInstance !.vcref.remove(0); - expect(directiveInstance !.vcref.indexOf(viewRef !)).toEqual(-1); - }); - }); - - describe('move', () => { - it('should move embedded views and associated DOM nodes without recreating them', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - createView('A'); - createView('B'); - createView('C'); - fixture.update(); - expect(fixture.html).toEqual('

        ABC'); - - // The DOM is manually modified here to ensure that the text node is actually moved - fixture.hostElement.childNodes[2].nodeValue = '**A**'; - expect(fixture.html).toEqual('

        **A**BC'); - - let viewRef = directiveInstance !.vcref.get(0); - directiveInstance !.vcref.move(viewRef !, 2); - fixture.update(); - expect(fixture.html).toEqual('

        BC**A**'); - - directiveInstance !.vcref.move(viewRef !, 0); - fixture.update(); - expect(fixture.html).toEqual('

        **A**BC'); - - directiveInstance !.vcref.move(viewRef !, 1); - fixture.update(); - expect(fixture.html).toEqual('

        B**A**C'); - - expect(() => { directiveInstance !.vcref.move(viewRef !, -1); }).toThrow(); - expect(() => { directiveInstance !.vcref.move(viewRef !, 42); }).toThrow(); - }); }); describe('createComponent', () => { let templateExecutionCounter = 0; - it('should work without Injector and NgModuleRef', () => { - class EmbeddedComponent { - constructor() {} - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp']], - factory: () => new EmbeddedComponent(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponent) => { - templateExecutionCounter++; - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - } - }); - } - - templateExecutionCounter = 0; - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

        '); - expect(templateExecutionCounter).toEqual(0); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent)); - fixture.update(); - expect(fixture.html).toEqual('

        foo'); - expect(templateExecutionCounter).toEqual(2); - - directiveInstance !.vcref.detach(0); - fixture.update(); - expect(fixture.html).toEqual('

        '); - expect(templateExecutionCounter).toEqual(2); - - directiveInstance !.vcref.insert(componentRef.hostView); - fixture.update(); - expect(fixture.html).toEqual('

        foo'); - expect(templateExecutionCounter).toEqual(3); - }); - - it('should work with NgModuleRef and Injector', () => { - class EmbeddedComponent { - constructor(public s: String) {} - - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp']], - factory: () => new EmbeddedComponent(ɵɵdirectiveInject(String)), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponent) => { - templateExecutionCounter++; - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'foo'); - } - } - }); - } - - class MyAppModule { - static ngInjectorDef = ɵɵdefineInjector({ - factory: () => new MyAppModule(), - imports: [], - providers: [ - {provide: APP_ROOT, useValue: true}, - {provide: RendererFactory2, useValue: getRendererFactory2(document)}, - {provide: String, useValue: 'module'} - ] - }); - static ngModuleDef: NgModuleDef = { bootstrap: [] } as any; - } - const myAppModuleFactory = new NgModuleFactory(MyAppModule); - const ngModuleRef = myAppModuleFactory.create(null); - - class SomeModule { - static ngInjectorDef = ɵɵdefineInjector({ - factory: () => new SomeModule(), - providers: [ - {provide: NgModuleRef, useValue: ngModuleRef}, - {provide: String, useValue: 'injector'} - ] - }); - } - const injector = createInjector(SomeModule); - - templateExecutionCounter = 0; - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

        '); - expect(templateExecutionCounter).toEqual(0); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, injector); - fixture.update(); - expect(fixture.html).toEqual('

        foo'); - expect(templateExecutionCounter).toEqual(2); - expect(componentRef.instance.s).toEqual('injector'); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined, - undefined, ngModuleRef); - fixture.update(); - expect(fixture.html) - .toEqual( - '

        foofoo'); - expect(templateExecutionCounter).toEqual(5); - }); - describe('ComponentRef', () => { let dynamicComp !: DynamicComp; @@ -1129,101 +311,6 @@ describe('ViewContainerRef', () => { }); }); - - class EmbeddedComponentWithNgContent { - static ngComponentDef = ɵɵdefineComponent({ - type: EmbeddedComponentWithNgContent, - encapsulation: ViewEncapsulation.None, - selectors: [['embedded-cmp-with-ngcontent']], - factory: () => new EmbeddedComponentWithNgContent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: EmbeddedComponentWithNgContent) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵprojection(0, 0); - ɵɵelement(1, 'hr'); - ɵɵprojection(2, 1); - } - } - }); - } - - it('should support projectable nodes', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

        '); - - const myNode = document.createElement('div'); - const myText = document.createTextNode('bar'); - const myText2 = document.createTextNode('baz'); - myNode.appendChild(myText); - myNode.appendChild(myText2); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, - undefined, [[myNode]]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

        barbaz

        '); - }); - - it('should support reprojection of projectable nodes', () => { - class Reprojector { - static ngComponentDef = ɵɵdefineComponent({ - type: Reprojector, - encapsulation: ViewEncapsulation.None, - selectors: [['reprojector']], - factory: () => new Reprojector(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Reprojector) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'embedded-cmp-with-ngcontent'); - { ɵɵprojection(1, 0); } - ɵɵelementEnd(); - } - }, - directives: [EmbeddedComponentWithNgContent] - }); - } - - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

        '); - - const myNode = document.createElement('div'); - const myText = document.createTextNode('bar'); - const myText2 = document.createTextNode('baz'); - myNode.appendChild(myText); - myNode.appendChild(myText2); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(Reprojector), 0, undefined, [[myNode]]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

        barbaz

        '); - }); - - it('should support many projectable nodes with many slots', () => { - const fixture = - new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('

        '); - - directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponentWithNgContent), 0, - undefined, [ - [document.createTextNode('1'), document.createTextNode('2')], - [document.createTextNode('3'), document.createTextNode('4')] - ]); - fixture.update(); - expect(fixture.html) - .toEqual( - '

        12
        34
        '); - }); }); describe('getters', () => { @@ -1261,657 +348,9 @@ describe('ViewContainerRef', () => { .toEqual('header-cmp'); expect(() => directiveInstance !.vcref.parentInjector.get(ElementRef)).toThrow(); }); - - it('should work on templates', () => { - function createTemplate() { - ɵɵtemplate(0, embeddedTemplate, 1, 1, 'ng-template', ['vcref', '']); - ɵɵelement(1, 'footer'); - } - - new TemplateFixture(createTemplate, () => {}, 2, 0, [DirectiveWithVCRef]); - expect(directiveInstance !.vcref.element.nativeElement.textContent).toEqual('container'); - expect(directiveInstance !.vcref.injector.get(ElementRef).nativeElement.textContent) - .toEqual('container'); - expect(() => directiveInstance !.vcref.parentInjector.get(ElementRef)).toThrow(); - }); }); }); - describe('projection', () => { - function embeddedTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'span'); - ɵɵtext(1); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ctx.name); - } - } - - it('should project the ViewContainerRef content along its host, in an element', () => { - @Component({selector: 'child', template: '
        '}) - class Child { - static ngComponentDef = ɵɵdefineComponent({ - type: Child, - encapsulation: ViewEncapsulation.None, - selectors: [['child']], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵelementStart(0, 'div'); - { ɵɵprojection(1); } - ɵɵelementEnd(); - } - } - }); - } - - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
        blah
        ` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵelementStart(2, 'child'); - { - ɵɵelementStart(3, 'header', ['vcref', '']); - { ɵɵtext(4, 'blah'); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - let tplRef: any; - if (rf & RenderFlags.Update) { - tplRef = ɵɵreference(1); - ɵɵelementProperty(3, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(3, 'name', ɵɵbind(cmp.name)); - } - }, - directives: [Child, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('
        blah
        '); - - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual('
        blah
        bar
        '); - }); - - it('should project the ViewContainerRef content along its host, in a view', () => { - @Component({ - selector: 'child-with-view', - template: ` - Before (inside)- - % if (show) { - - % } - After (inside) - ` - }) - class ChildWithView { - show: boolean = true; - static ngComponentDef = ɵɵdefineComponent({ - type: ChildWithView, - encapsulation: ViewEncapsulation.None, - selectors: [['child-with-view']], - factory: () => new ChildWithView(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: ChildWithView) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef(); - ɵɵtext(0, 'Before (inside)-'); - ɵɵcontainer(1); - ɵɵtext(2, 'After (inside)'); - } - if (rf & RenderFlags.Update) { - ɵɵcontainerRefreshStart(1); - if (cmp.show) { - let rf0 = ɵɵembeddedViewStart(0, 1, 0); - if (rf0 & RenderFlags.Create) { - ɵɵprojection(0); - } - ɵɵembeddedViewEnd(); - } - ɵɵcontainerRefreshEnd(); - } - } - }); - } - - @Component({ - selector: 'parent', - template: ` - - {{name}} - - - Before projected -
        blah
        - After projected -
        ` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', undefined, ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵelementStart(2, 'child-with-view'); - ɵɵtext(3, 'Before projected'); - ɵɵelementStart(4, 'header', ['vcref', '']); - ɵɵtext(5, 'blah'); - ɵɵelementEnd(); - ɵɵtext(6, 'After projected-'); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(4, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(4, 'name', ɵɵbind(cmp.name)); - } - }, - directives: [ChildWithView, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - 'Before (inside)-Before projected
        blah
        After projected-After (inside)
        '); - - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - 'Before (inside)-Before projected
        blah
        barAfter projected-After (inside)
        '); - }); - - describe('with select', () => { - @Component({ - selector: 'child-with-selector', - template: ` - - ` - }) - class ChildWithSelector { - static ngComponentDef = ɵɵdefineComponent({ - type: ChildWithSelector, - encapsulation: ViewEncapsulation.None, - selectors: [['child-with-selector']], - factory: () => new ChildWithSelector(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: ChildWithSelector) => { - if (rf & RenderFlags.Create) { - ɵɵprojectionDef([[['header']]]); - ɵɵelementStart(0, 'first'); - { ɵɵprojection(1, 1); } - ɵɵelementEnd(); - ɵɵelementStart(2, 'second'); - { ɵɵprojection(3); } - ɵɵelementEnd(); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - it('should project the ViewContainerRef content along its host, when the host matches a selector', - () => { - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
        blah
        ` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - let tplRef: any; - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵelementStart(2, 'child-with-selector'); - ɵɵelementStart(3, 'header', ['vcref', '']); - ɵɵtext(4, 'blah'); - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - tplRef = ɵɵreference(1); - ɵɵelementProperty(3, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(3, 'name', ɵɵbind(cmp.name)); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '
        blah
        '); - - directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '
        blah
        bar
        '); - }); - - it('should not project the ViewContainerRef content, when the host does not match a selector', - () => { - @Component({ - selector: 'parent', - template: ` - - {{name}} - -
        blah
        ` - }) - class Parent { - name: string = 'bar'; - static ngComponentDef = ɵɵdefineComponent({ - type: Parent, - encapsulation: ViewEncapsulation.None, - selectors: [['parent']], - factory: () => new Parent(), - consts: 5, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - let tplRef: any; - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, embeddedTemplate, 2, 1, 'ng-template', null, ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵelementStart(2, 'child-with-selector'); - ɵɵelementStart(3, 'footer', ['vcref', '']); - ɵɵtext(4, 'blah'); - ɵɵelementEnd(); - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - tplRef = ɵɵreference(1); - ɵɵelementProperty(3, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(3, 'name', ɵɵbind(cmp.name)); - } - }, - directives: [ChildWithSelector, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '
        blah
        '); - - directiveInstance !.vcref.createEmbeddedView( - directiveInstance !.tplRef, fixture.component); - fixture.update(); - expect(fixture.html) - .toEqual( - '
        blah
        bar
        '); - }); - }); - }); - - describe('life cycle hooks', () => { - - // Angular 5 reference: https://stackblitz.com/edit/lifecycle-hooks-vcref - const log: string[] = []; - - @Component({selector: 'hooks', template: `{{name}}`}) - class ComponentWithHooks { - // TODO(issue/24571): remove '!'. - name !: string; - - private log(msg: string) { log.push(msg); } - - ngOnChanges() { this.log('onChanges-' + this.name); } - ngOnInit() { this.log('onInit-' + this.name); } - ngDoCheck() { this.log('doCheck-' + this.name); } - - ngAfterContentInit() { this.log('afterContentInit-' + this.name); } - ngAfterContentChecked() { this.log('afterContentChecked-' + this.name); } - - ngAfterViewInit() { this.log('afterViewInit-' + this.name); } - ngAfterViewChecked() { this.log('afterViewChecked-' + this.name); } - - ngOnDestroy() { this.log('onDestroy-' + this.name); } - - static ngComponentDef = ɵɵdefineComponent({ - type: ComponentWithHooks, - encapsulation: ViewEncapsulation.None, - selectors: [['hooks']], - factory: () => new ComponentWithHooks(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, cmp: ComponentWithHooks) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('', cmp.name, '')); - } - }, - features: [ɵɵNgOnChangesFeature()], - inputs: {name: 'name'} - }); - } - - it('should call all hooks in correct order when creating with createEmbeddedView', () => { - function SomeComponent_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'hooks'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'name', ɵɵbind('C')); - } - } - - @Component({ - template: ` - - - - - - ` - }) - class SomeComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponent, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 4, - vars: 3, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, SomeComponent_Template_0, 1, 1, 'ng-template', [], ['foo', ''], - ɵɵtemplateRefExtractor); - ɵɵelement(2, 'hooks', ['vcref', '']); - ɵɵelement(3, 'hooks'); - } - if (rf & RenderFlags.Update) { - const tplRef = ɵɵreference(1); - ɵɵelementProperty(2, 'tplRef', ɵɵbind(tplRef)); - ɵɵelementProperty(2, 'name', ɵɵbind('A')); - ɵɵelementProperty(3, 'name', ɵɵbind('B')); - } - }, - directives: [ComponentWithHooks, DirectiveWithVCRef], - features: [ɵɵNgOnChangesFeature()], - }); - } - - log.length = 0; - - const fixture = new ComponentFixture(SomeComponent); - expect(log).toEqual([ - 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', - 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', - 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', - 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); - expect(fixture.html).toEqual('AB'); - expect(log).toEqual([]); - - log.length = 0; - fixture.update(); - expect(fixture.html).toEqual('ACB'); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onChanges-C', 'onInit-C', 'doCheck-C', 'afterContentInit-C', - 'afterContentChecked-C', 'afterViewInit-C', 'afterViewChecked-C', 'afterContentChecked-A', - 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const viewRef = directiveInstance !.vcref.detach(0); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.insert(viewRef !); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-C', 'afterContentChecked-C', 'afterViewChecked-C', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(log).toEqual([ - 'onDestroy-C', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - }); - - it('should call all hooks in correct order when creating with createComponent', () => { - @Component({ - template: ` - - - ` - }) - class SomeComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: SomeComponent, - encapsulation: ViewEncapsulation.None, - selectors: [['some-comp']], - factory: () => new SomeComponent(), - consts: 2, - vars: 2, - template: (rf: RenderFlags, cmp: SomeComponent) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'hooks', ['vcref', '']); - ɵɵelement(1, 'hooks'); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'name', ɵɵbind('A')); - ɵɵelementProperty(1, 'name', ɵɵbind('B')); - } - }, - directives: [ComponentWithHooks, DirectiveWithVCRef], - features: [ɵɵNgOnChangesFeature()], - }); - } - - log.length = 0; - - const fixture = new ComponentFixture(SomeComponent); - expect(log).toEqual([ - 'onChanges-A', 'onInit-A', 'doCheck-A', 'onChanges-B', 'onInit-B', 'doCheck-B', - 'afterContentInit-A', 'afterContentChecked-A', 'afterContentInit-B', - 'afterContentChecked-B', 'afterViewInit-A', 'afterViewChecked-A', 'afterViewInit-B', - 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(ComponentWithHooks)); - expect(fixture.html).toEqual('AB'); - expect(log).toEqual([]); - - componentRef.instance.name = 'D'; - log.length = 0; - fixture.update(); - expect(fixture.html).toEqual('ADB'); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', - 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', - 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - const viewRef = directiveInstance !.vcref.detach(0); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.insert(viewRef !); - fixture.update(); - expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'doCheck-D', 'afterContentChecked-D', 'afterViewChecked-D', - 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' - ]); - - log.length = 0; - directiveInstance !.vcref.remove(0); - fixture.update(); - expect(log).toEqual([ - 'onDestroy-D', 'doCheck-A', 'doCheck-B', 'afterContentChecked-A', 'afterContentChecked-B', - 'afterViewChecked-A', 'afterViewChecked-B' - ]); - }); - }); - - describe('host bindings', () => { - - it('should support host bindings on dynamically created components', () => { - - @Component( - {selector: 'host-bindings', host: {'id': 'attribute', '[title]': 'title'}, template: ``}) - class HostBindingCmpt { - title = 'initial'; - - static ngComponentDef = ɵɵdefineComponent({ - type: HostBindingCmpt, - selectors: [['host-bindings']], - factory: () => new HostBindingCmpt(), - consts: 0, - vars: 0, - template: (rf: RenderFlags, cmp: HostBindingCmpt) => {}, - hostBindings: function(rf: RenderFlags, ctx: HostBindingCmpt, elIndex: number) { - if (rf & RenderFlags.Create) { - ɵɵelementHostAttrs(['id', 'attribute']); - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elIndex, 'title', ɵɵbind(ctx.title)); - } - }, - }); - } - - @Component({ - template: ` - - ` - }) - class AppCmpt { - static ngComponentDef = ɵɵdefineComponent({ - type: AppCmpt, - selectors: [['app']], - factory: () => new AppCmpt(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: AppCmpt) => { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, null, 0, 0, 'ng-template', ['vcref', '']); - } - }, - directives: [HostBindingCmpt, DirectiveWithVCRef] - }); - } - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.html).toBe(''); - - const componentRef = directiveInstance !.vcref.createComponent( - directiveInstance !.cfr.resolveComponentFactory(HostBindingCmpt)); - fixture.update(); - expect(fixture.html).toBe(''); - - - componentRef.instance.title = 'changed'; - fixture.update(); - expect(fixture.html).toBe(''); - }); - - }); - describe('view engine compatibility', () => { @Component({selector: 'app', template: ''}) @@ -1977,79 +416,6 @@ describe('ViewContainerRef', () => { expect(parentInjector.get('foo')).toEqual('bar'); }); - it('should check bindings for components dynamically created by root component', () => { - class DynamicCompWithBindings { - checkCount = 0; - - ngDoCheck() { this.checkCount++; } - - /** check count: {{ checkCount }} */ - static ngComponentDef = ɵɵdefineComponent({ - type: DynamicCompWithBindings, - selectors: [['dynamic-cmpt-with-bindings']], - factory: () => new DynamicCompWithBindings(), - consts: 1, - vars: 1, - template: (rf: RenderFlags, ctx: DynamicCompWithBindings) => { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵinterpolation1('check count: ', ctx.checkCount, '')); - } - } - }); - } - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.outerHtml).toBe('
        '); - - fixture.component.insert(DynamicCompWithBindings); - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
        check count: 1'); - - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
        check count: 2'); - }); - - it('should create deep DOM tree immediately for dynamically created components', () => { - let name = 'text'; - const Child = createComponent('child', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { ɵɵtext(1); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(1, ɵɵbind(name)); - } - }, 2, 1); - - const DynamicCompWithChildren = - createComponent('dynamic-cmpt-with-children', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'child'); - } - }, 1, 0, [Child]); - - const fixture = new ComponentFixture(AppCmpt); - expect(fixture.outerHtml).toBe('
        '); - - fixture.component.insert(DynamicCompWithChildren); - expect(fixture.outerHtml) - .toBe( - '
        '); - - fixture.update(); - expect(fixture.outerHtml) - .toBe( - '
        text
        '); - }); - it('should support view queries for dynamically created components', () => { let dynamicComp !: DynamicCompWithViewQueries; let fooEl !: RElement; diff --git a/packages/core/test/sanitization/style_sanitizer_spec.ts b/packages/core/test/sanitization/style_sanitizer_spec.ts index e5a9d200e6..5adafceb8e 100644 --- a/packages/core/test/sanitization/style_sanitizer_spec.ts +++ b/packages/core/test/sanitization/style_sanitizer_spec.ts @@ -32,7 +32,7 @@ import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer'; expectSanitize('rgb(255, 0, 0)').toEqual('rgb(255, 0, 0)'); expectSanitize('expression(haha)').toEqual('unsafe'); }); - t.it('rejects unbalanced quotes', () => { expectSanitize('"value" "').toEqual('unsafe'); }); + t.it('rejects unblanaced quotes', () => { expectSanitize('"value" "').toEqual('unsafe'); }); t.it('accepts transform functions', () => { expectSanitize('rotate(90deg)').toEqual('rotate(90deg)'); expectSanitize('rotate(javascript:evil())').toEqual('unsafe'); @@ -58,7 +58,6 @@ import {_sanitizeStyle} from '../../src/sanitization/style_sanitizer'; t.it('accepts quoted URLs', () => { expectSanitize('url("foo/bar.png")').toEqual('url("foo/bar.png")'); expectSanitize(`url('foo/bar.png')`).toEqual(`url('foo/bar.png')`); - expectSanitize(`url('foo/bar (1).png')`).toEqual(`url('foo/bar (1).png')`); expectSanitize(`url( 'foo/bar.png'\n )`).toEqual(`url( 'foo/bar.png'\n )`); expectSanitize('url("javascript:evil()")').toEqual('unsafe'); expectSanitize('url( " javascript:evil() " )').toEqual('unsafe'); diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index fa546d6ac1..61b809e2e3 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Input, NgModule, Optional, Pipe, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵtext as text} from '@angular/core'; +import {Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Input, NgModule, Optional, Pipe, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineNgModule as defineNgModule, ɵɵtext as text} from '@angular/core'; import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -371,6 +371,62 @@ describe('TestBed', () => { }).toThrowError(); }); + onlyInIvy('TestBed new feature to allow declaration and import of component') + .it('should allow both the declaration and import of a component into the testing module', + () => { + // This test validates that a component (Outer) which is both declared and imported + // (via its module) in the testing module behaves correctly. That is: + // + // 1) the component should be compiled in the scope of its original module. + // + // This condition is tested by having the component (Outer) use another component + // (Inner) within its template. Thus, if it's compiled in the correct scope then the + // text 'Inner' from the template of (Inner) should appear in the result. + // + // 2) the component should be available in the TestingModule scope. + // + // This condition is tested by attempting to use the component (Outer) inside a test + // fixture component (Fixture) which is declared in the testing module only. + + @Component({ + selector: 'inner', + template: 'Inner', + }) + class Inner { + } + + @Component({ + selector: 'outer', + template: '', + }) + class Outer { + } + + @NgModule({ + declarations: [Inner, Outer], + }) + class Module { + } + + @Component({ + template: '', + selector: 'fixture', + }) + class Fixture { + } + + TestBed.configureTestingModule({ + declarations: [Outer, Fixture], + imports: [Module], + }); + + const fixture = TestBed.createComponent(Fixture); + // The Outer component should have its template stamped out, and that template should + // include a correct instance of the Inner component with the 'Inner' text from its + // template. + expect(fixture.nativeElement.innerHTML).toEqual('Inner'); + }); + onlyInIvy('TestBed should handle AOT pre-compiled Components') .describe('AOT pre-compiled components', () => { /** @@ -423,6 +479,50 @@ describe('TestBed', () => { const fixture = TestBed.createComponent(SomeComponent); expect(fixture.nativeElement.innerHTML).toBe('Template override'); }); + + it('should have an ability to override template with empty string', () => { + const SomeComponent = getAOTCompiledComponent(); + TestBed.configureTestingModule({declarations: [SomeComponent]}); + TestBed.overrideTemplateUsingTestingModule(SomeComponent, ''); + const fixture = TestBed.createComponent(SomeComponent); + expect(fixture.nativeElement.innerHTML).toBe(''); + }); + + it('should allow component in both in declarations and imports', () => { + const SomeComponent = getAOTCompiledComponent(); + + // This is an AOT compiled module which declares (but does not export) SomeComponent. + class ModuleClass { + static ngModuleDef = defineNgModule({ + type: ModuleClass, + declarations: [SomeComponent], + }); + } + + @Component({ + template: '', + + selector: 'fixture', + }) + class TestFixture { + } + + TestBed.configureTestingModule({ + // Here, SomeComponent is both declared, and then the module which declares it is + // also imported. This used to be a duplicate declaration error, but is now interpreted + // to mean: + // 1) Compile (or reuse) SomeComponent in the context of its original NgModule + // 2) Make SomeComponent available in the scope of the testing module, even if it wasn't + // originally exported from its NgModule. + // + // This allows TestFixture to use SomeComponent, which is asserted below. + declarations: [SomeComponent, TestFixture], + imports: [ModuleClass], + }); + const fixture = TestBed.createComponent(TestFixture); + // The regex avoids any issues with styling attributes. + expect(fixture.nativeElement.innerHTML).toMatch(/]*>Some template<\/comp>/); + }); }); onlyInIvy('patched ng defs should be removed after resetting TestingModule') diff --git a/packages/core/testing/src/r3_test_bed.ts b/packages/core/testing/src/r3_test_bed.ts index 38f8e50abc..c14143aa36 100644 --- a/packages/core/testing/src/r3_test_bed.ts +++ b/packages/core/testing/src/r3_test_bed.ts @@ -150,25 +150,6 @@ export class TestBedRender3 implements Injector, TestBed { return TestBedRender3 as any as TestBedStatic; } - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - static deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - static deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - static deprecatedOverrideProvider(token: any, provider: { - useFactory?: Function, - useValue?: any, - deps?: any[], - }): TestBedStatic { - _getTestBedRender3().deprecatedOverrideProvider(token, provider as any); - return TestBedRender3 as any as TestBedStatic; - } - static get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; /** * @deprecated from v8.0.0 use Type or InjectionToken @@ -317,30 +298,6 @@ export class TestBedRender3 implements Injector, TestBed { this.compiler.overrideProvider(token, provider); } - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - deprecatedOverrideProvider( - token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { - // HACK: This is NOT the correct implementation for deprecatedOverrideProvider. - // To implement it in a backward compatible way, we would need to record some state - // so we know to prevent eager instantiation of NgModules. However, we don't plan - // to implement this at all since the API is deprecated and scheduled for removal - // in V8. This hack is here temporarily for Ivy testing until we transition apps - // inside Google to the overrideProvider API. At that point, we will be able to - // remove this method entirely. In the meantime, we can use overrideProvider to - // test apps with Ivy that don't care about eager instantiation. This fixes 85% - // of cases in our blueprint. - this.overrideProvider(token, provider as any); - } - createComponent(type: Type): ComponentFixture { const testComponentRenderer: TestComponentRenderer = this.get(TestComponentRenderer); const rootElId = `root-ng-internal-isolated-${_nextRootElementId++}`; diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index e18e7f81fa..5943e89717 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -6,17 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgZone, Injector, Pipe, PlatformRef, Provider, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, ɵNG_MODULE_DEF as NG_MODULE_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵɵInjectableDef as InjectableDef, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵDirectiveDef as DirectiveDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵtransitiveScopesFor as transitiveScopesFor,} from '@angular/core'; import {ResourceLoader} from '@angular/compiler'; +import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, Injector, LOCALE_ID, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, Type, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, ɵNG_MODULE_DEF as NG_MODULE_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core'; -import {clearResolutionOfComponentResourcesQueue, restoreComponentResolutionQueue, resolveComponentResources, isComponentDefPendingResolution} from '../../src/metadata/resource_loading'; - +import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; import {MetadataOverride} from './metadata_override'; import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers'; import {TestModuleMetadata} from './test_bed_common'; -const TESTING_MODULE = 'TestingModule'; -type TESTING_MODULE = typeof TESTING_MODULE; +enum TestingModuleOverride { + DECLARATION, + OVERRIDE_TEMPLATE, +} + +function isTestingModuleOverride(value: unknown): value is TestingModuleOverride { + return value === TestingModuleOverride.DECLARATION || + value === TestingModuleOverride.OVERRIDE_TEMPLATE; +} // Resolvers for Angular decorators type Resolvers = { @@ -56,7 +62,7 @@ export class R3TestBedCompiler { private resolvers: Resolvers = initResolvers(); - private componentToModuleScope = new Map, Type|TESTING_MODULE>(); + private componentToModuleScope = new Map, Type|TestingModuleOverride>(); // Map that keeps initial version of component/directive/pipe defs in case // we compile a Type again, thus overriding respective static fields. This is @@ -92,7 +98,7 @@ export class R3TestBedCompiler { configureTestingModule(moduleDef: TestModuleMetadata): void { // Enqueue any compilation tasks for the directly declared component. if (moduleDef.declarations !== undefined) { - this.queueTypeArray(moduleDef.declarations, TESTING_MODULE); + this.queueTypeArray(moduleDef.declarations, TestingModuleOverride.DECLARATION); this.declarations.push(...moduleDef.declarations); } @@ -188,7 +194,7 @@ export class R3TestBedCompiler { } // Set the component's scope to be the testing module. - this.componentToModuleScope.set(type, TESTING_MODULE); + this.componentToModuleScope.set(type, TestingModuleOverride.OVERRIDE_TEMPLATE); } async compileComponents(): Promise { @@ -231,6 +237,9 @@ export class R3TestBedCompiler { const parentInjector = this.platform.injector; this.testModuleRef = new NgModuleRef(this.testModuleType, parentInjector); + // Set the locale ID, it can be overridden for the tests + const localeId = this.testModuleRef.injector.get(LOCALE_ID, DEFAULT_LOCALE_ID); + setLocaleId(localeId); // ApplicationInitStatus.runInitializers() is marked @internal to core. // Cast it to any before accessing it. @@ -306,14 +315,15 @@ export class R3TestBedCompiler { } private applyTransitiveScopes(): void { - const moduleToScope = new Map|TESTING_MODULE, NgModuleTransitiveScopes>(); - const getScopeOfModule = (moduleType: Type| TESTING_MODULE): NgModuleTransitiveScopes => { - if (!moduleToScope.has(moduleType)) { - const realType = moduleType === TESTING_MODULE ? this.testModuleType : moduleType; - moduleToScope.set(moduleType, transitiveScopesFor(realType)); - } - return moduleToScope.get(moduleType) !; - }; + const moduleToScope = new Map|TestingModuleOverride, NgModuleTransitiveScopes>(); + const getScopeOfModule = + (moduleType: Type| TestingModuleOverride): NgModuleTransitiveScopes => { + if (!moduleToScope.has(moduleType)) { + const realType = isTestingModuleOverride(moduleType) ? this.testModuleType : moduleType; + moduleToScope.set(moduleType, transitiveScopesFor(realType)); + } + return moduleToScope.get(moduleType) !; + }; this.componentToModuleScope.forEach((moduleType, componentType) => { const moduleScope = getScopeOfModule(moduleType); @@ -370,7 +380,7 @@ export class R3TestBedCompiler { this.existingComponentStyles.clear(); } - private queueTypeArray(arr: any[], moduleType: Type|TESTING_MODULE): void { + private queueTypeArray(arr: any[], moduleType: Type|TestingModuleOverride): void { for (const value of arr) { if (Array.isArray(value)) { this.queueTypeArray(value, moduleType); @@ -392,7 +402,7 @@ export class R3TestBedCompiler { compileNgModuleDefs(ngModule as NgModuleType, metadata); } - private queueType(type: Type, moduleType: Type|TESTING_MODULE): void { + private queueType(type: Type, moduleType: Type|TestingModuleOverride): void { const component = this.resolvers.component.resolve(type); if (component) { // Check whether a give Type has respective NG def (ngComponentDef) and compile if def is @@ -404,9 +414,22 @@ export class R3TestBedCompiler { this.seenComponents.add(type); // Keep track of the module which declares this component, so later the component's scope - // can be set correctly. Only record this the first time, because it might be overridden by - // overrideTemplateUsingTestingModule. - if (!this.componentToModuleScope.has(type)) { + // can be set correctly. If the component has already been recorded here, then one of several + // cases is true: + // * the module containing the component was imported multiple times (common). + // * the component is declared in multiple modules (which is an error). + // * the component was in 'declarations' of the testing module, and also in an imported module + // in which case the module scope will be TestingModuleOverride.DECLARATION. + // * overrideTemplateUsingTestingModule was called for the component in which case the module + // scope will be TestingModuleOverride.OVERRIDE_TEMPLATE. + // + // If the component was previously in the testing module's 'declarations' (meaning the + // current value is TestingModuleOverride.DECLARATION), then `moduleType` is the component's + // real module, which was imported. This pattern is understood to mean that the component + // should use its original scope, but that the testing module should also contain the + // component in its scope. + if (!this.componentToModuleScope.has(type) || + this.componentToModuleScope.get(type) === TestingModuleOverride.DECLARATION) { this.componentToModuleScope.set(type, moduleType); } return; @@ -502,6 +525,8 @@ export class R3TestBedCompiler { this.initialNgDefs.clear(); this.moduleProvidersOverridden.clear(); this.restoreComponentResolutionQueue(); + // Restore the locale ID to the default value, this shouldn't be necessary but we never know + setLocaleId(DEFAULT_LOCALE_ID); } private compileTestModule(): void { @@ -525,7 +550,7 @@ export class R3TestBedCompiler { imports, schemas: this.schemas, providers, - }); + }, /* allowDuplicateDeclarationsInRoot */ true); // clang-format on this.applyProviderOverridesToModule(this.testModuleType); diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 77961d62bb..6fe51195c9 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -57,8 +57,14 @@ export interface TestBed { compileComponents(): Promise; get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; + + // TODO: switch back to official deprecation marker once TSLint issue is resolved + // https://github.com/palantir/tslint/issues/4522 /** - * @deprecated from v8.0.0 use Type or InjectionToken + * deprecated from v8.0.0 use Type or InjectionToken + * This does not use the deprecated jsdoc tag on purpose + * because it renders all overloads as deprecated in TSLint + * due to https://github.com/palantir/tslint/issues/4522. */ get(token: any, notFoundValue?: any): any; @@ -83,20 +89,6 @@ export interface TestBed { overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void; - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - deprecatedOverrideProvider( - token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void; - - overrideTemplateUsingTestingModule(component: Type, template: string): void; createComponent(component: Type): ComponentFixture; @@ -224,25 +216,6 @@ export class TestBedViewEngine implements Injector, TestBed { return TestBedViewEngine as any as TestBedStatic; } - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - static deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - static deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - static deprecatedOverrideProvider(token: any, provider: { - useFactory?: Function, - useValue?: any, - deps?: any[], - }): TestBedStatic { - _getTestBedViewEngine().deprecatedOverrideProvider(token, provider as any); - return TestBedViewEngine as any as TestBedStatic; - } - static get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; /** * @deprecated from v8.0.0 use Type or InjectionToken @@ -537,21 +510,6 @@ export class TestBedViewEngine implements Injector, TestBed { this.overrideProviderImpl(token, provider); } - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - deprecatedOverrideProvider( - token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { - this.overrideProviderImpl(token, provider, /* deprecated */ true); - } - private overrideProviderImpl( token: any, provider: { useFactory?: Function, diff --git a/packages/core/testing/src/test_bed_common.ts b/packages/core/testing/src/test_bed_common.ts index 822cc15f9e..9977194ef1 100644 --- a/packages/core/testing/src/test_bed_common.ts +++ b/packages/core/testing/src/test_bed_common.ts @@ -114,22 +114,6 @@ export interface TestBedStatic { deps?: any[], }): TestBedStatic; - /** - * Overwrites all providers for the given token with the given provider definition. - * - * @deprecated as it makes all NgModules lazy. Introduced only for migrating off of it. - */ - deprecatedOverrideProvider(token: any, provider: { - useFactory: Function, - deps: any[], - }): void; - deprecatedOverrideProvider(token: any, provider: {useValue: any;}): void; - deprecatedOverrideProvider(token: any, provider: { - useFactory?: Function, - useValue?: any, - deps?: any[], - }): TestBedStatic; - get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; /** * @deprecated from v8.0.0 use Type or InjectionToken diff --git a/packages/elements/schematics/ng-add/index_spec.ts b/packages/elements/schematics/ng-add/index_spec.ts index 1a4cf30a40..674a4584c2 100644 --- a/packages/elements/schematics/ng-add/index_spec.ts +++ b/packages/elements/schematics/ng-add/index_spec.ts @@ -48,8 +48,9 @@ describe('Elements Schematics', () => { .subscribe((tree: UnitTestTree) => appTree = tree, done.fail, done); }); - it('should run the ng-add schematic', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); + it('should run the ng-add schematic', async() => { + const tree = + await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise(); const configText = tree.readContent('/angular.json'); const config = JSON.parse(configText); const scripts = config.projects.elements.architect.build.options.scripts; diff --git a/packages/elements/src/utils.ts b/packages/elements/src/utils.ts index 0491485777..2333eb40b3 100644 --- a/packages/elements/src/utils.ts +++ b/packages/elements/src/utils.ts @@ -7,9 +7,11 @@ */ import {ComponentFactoryResolver, Injector, Type} from '@angular/core'; -const elProto = Element.prototype as any; -const matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector || - elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector; +const matches = (() => { + const elProto = Element.prototype as any; + return elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector || + elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector; +})(); /** * Provide methods for scheduling the execution of a callback. diff --git a/packages/examples/common/ngIf/ts/module.ts b/packages/examples/common/ngIf/ts/module.ts index 229ae2e666..913b6e3dc3 100644 --- a/packages/examples/common/ngIf/ts/module.ts +++ b/packages/examples/common/ngIf/ts/module.ts @@ -62,7 +62,7 @@ export class NgIfThenElse implements OnInit { @ViewChild('primaryBlock', {static: true}) primaryBlock: TemplateRef|null = null; - @ViewChild('secondaryBlock') + @ViewChild('secondaryBlock', {static: true}) secondaryBlock: TemplateRef|null = null; switchPrimary() { diff --git a/packages/examples/core/di/ts/contentChild/content_child_example.ts b/packages/examples/core/di/ts/contentChild/content_child_example.ts index a734781912..28928cc4ad 100644 --- a/packages/examples/core/di/ts/contentChild/content_child_example.ts +++ b/packages/examples/core/di/ts/contentChild/content_child_example.ts @@ -23,7 +23,7 @@ export class Pane { }) export class Tab { // TODO(issue/24571): remove '!'. - @ContentChild(Pane) pane !: Pane; + @ContentChild(Pane, {static: false}) pane !: Pane; } @Component({ diff --git a/packages/examples/core/di/ts/contentChild/content_child_howto.ts b/packages/examples/core/di/ts/contentChild/content_child_howto.ts index 5bcaa1afda..455979bfd0 100644 --- a/packages/examples/core/di/ts/contentChild/content_child_howto.ts +++ b/packages/examples/core/di/ts/contentChild/content_child_howto.ts @@ -15,10 +15,10 @@ class ChildDirective { @Directive({selector: 'someDir'}) class SomeDir implements AfterContentInit { - @ContentChild(ChildDirective) contentChild !: ChildDirective; + @ContentChild(ChildDirective, {static: false}) contentChild !: ChildDirective; ngAfterContentInit() { // contentChild is set } } -// #enddocregion \ No newline at end of file +// #enddocregion diff --git a/packages/examples/core/di/ts/viewChild/view_child_example.ts b/packages/examples/core/di/ts/viewChild/view_child_example.ts index 4f7ccd15b1..c624cf87e3 100644 --- a/packages/examples/core/di/ts/viewChild/view_child_example.ts +++ b/packages/examples/core/di/ts/viewChild/view_child_example.ts @@ -27,7 +27,7 @@ export class Pane { `, }) export class ViewChildComp { - @ViewChild(Pane) + @ViewChild(Pane, {static: false}) set pane(v: Pane) { setTimeout(() => { this.selectedPane = v.id; }, 0); } diff --git a/packages/examples/core/di/ts/viewChild/view_child_howto.ts b/packages/examples/core/di/ts/viewChild/view_child_howto.ts index a0753ddbbb..f3d74ac953 100644 --- a/packages/examples/core/di/ts/viewChild/view_child_howto.ts +++ b/packages/examples/core/di/ts/viewChild/view_child_howto.ts @@ -16,10 +16,10 @@ class ChildDirective { @Component({selector: 'someCmp', templateUrl: 'someCmp.html'}) class SomeCmp implements AfterViewInit { // TODO(issue/24571): remove '!'. - @ViewChild(ChildDirective) child !: ChildDirective; + @ViewChild(ChildDirective, {static: false}) child !: ChildDirective; ngAfterViewInit() { // child is set } } -// #enddocregion \ No newline at end of file +// #enddocregion diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 4f13da164b..6b89260de2 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -23,7 +23,7 @@ export const formDirectiveProvider: any = { useExisting: forwardRef(() => NgForm) }; -const resolvedPromise = Promise.resolve(null); +const resolvedPromise = (() => Promise.resolve(null))(); /** * @description diff --git a/packages/forms/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index 1f837f7858..3fd5ee5959 100644 --- a/packages/forms/src/directives/ng_model.ts +++ b/packages/forms/src/directives/ng_model.ts @@ -58,7 +58,7 @@ export const formControlBinding: any = { * 当在指令中使用 `exportAs` 时,这是一个常见问题! * */ -const resolvedPromise = Promise.resolve(null); +const resolvedPromise = (() => Promise.resolve(null))(); /** * @description diff --git a/packages/http/src/static_request.ts b/packages/http/src/static_request.ts index f55fa8acd0..c48cd252cb 100644 --- a/packages/http/src/static_request.ts +++ b/packages/http/src/static_request.ts @@ -185,7 +185,7 @@ function urlEncodeParams(params: {[key: string]: any}): URLSearchParams { const noop = function() {}; const w = typeof window == 'object' ? window : noop; -const FormData = (w as any /** TODO #9100 */)['FormData'] || noop; -const Blob = (w as any /** TODO #9100 */)['Blob'] || noop; +const FormData = (() => (w as any /** TODO #9100 */)['FormData'] || noop)(); +const Blob = (() => (w as any /** TODO #9100 */)['Blob'] || noop)(); export const ArrayBuffer: ArrayBufferConstructor = - (w as any /** TODO #9100 */)['ArrayBuffer'] || noop; + (() => (w as any /** TODO #9100 */)['ArrayBuffer'] || noop)(); diff --git a/packages/language-service/src/language_service.ts b/packages/language-service/src/language_service.ts index c76a3039ed..94e0bcc56c 100644 --- a/packages/language-service/src/language_service.ts +++ b/packages/language-service/src/language_service.ts @@ -13,7 +13,7 @@ import {getTemplateCompletions} from './completions'; import {getDefinition} from './definitions'; import {getDeclarationDiagnostics, getTemplateDiagnostics} from './diagnostics'; import {getHover} from './hover'; -import {Completions, Definition, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Pipes, Span, TemplateSource} from './types'; +import {Completions, Definition, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Span, TemplateSource} from './types'; /** diff --git a/packages/language-service/src/reflector_host.ts b/packages/language-service/src/reflector_host.ts index dcf66f2fae..5fcbb57286 100644 --- a/packages/language-service/src/reflector_host.ts +++ b/packages/language-service/src/reflector_host.ts @@ -48,7 +48,6 @@ class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost, Me } export class ReflectorHost implements StaticSymbolResolverHost { - private moduleResolutionCache: ts.ModuleResolutionCache; private hostAdapter: ReflectorModuleModuleResolutionHost; private metadataReaderCache = createMetadataReaderCache(); @@ -56,8 +55,6 @@ export class ReflectorHost implements StaticSymbolResolverHost { getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost, private options: CompilerOptions) { this.hostAdapter = new ReflectorModuleModuleResolutionHost(serviceHost, getProgram); - this.moduleResolutionCache = - ts.createModuleResolutionCache(serviceHost.getCurrentDirectory(), (s) => s); } getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined { diff --git a/packages/language-service/src/typescript_host.ts b/packages/language-service/src/typescript_host.ts index b5a26f7fcc..a3fa6eeaeb 100644 --- a/packages/language-service/src/typescript_host.ts +++ b/packages/language-service/src/typescript_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; +import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, analyzeNgModules, createOfflineCompileUrlResolver, isFormattedError} from '@angular/compiler'; import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; diff --git a/packages/language-service/test/BUILD.bazel b/packages/language-service/test/BUILD.bazel index 0348a26aac..4529a90fa3 100644 --- a/packages/language-service/test/BUILD.bazel +++ b/packages/language-service/test/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/test:test_utils", "//packages/language-service", + "@npm//reflect-metadata", "@npm//typescript", ], ) diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index 6b4dda17c9..d54624b7c9 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -86,12 +86,10 @@ describe('completions', () => { expect(() => { let chance = 0.05; - let requests = 0; function tryCompletionsAt(position: number) { try { if (Math.random() < chance) { ngService.getCompletionsAt(fileName, position); - requests++; } } catch (e) { // Emit enough diagnostic information to reproduce the error. diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index c756a995f6..892972034b 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -103,7 +103,7 @@ describe('diagnostics', () => { it('should not report an error for a form\'s host directives', () => { const code = '\n@Component({template: \'
        \'}) export class MyComponent {}'; - addCode(code, (fileName, content) => { + addCode(code, fileName => { const diagnostics = ngService.getDiagnostics(fileName); expectOnlyModuleDiagnostics(diagnostics); }); diff --git a/packages/language-service/test/language_service_spec.ts b/packages/language-service/test/language_service_spec.ts index 38d3de1277..43a5fc45ca 100644 --- a/packages/language-service/test/language_service_spec.ts +++ b/packages/language-service/test/language_service_spec.ts @@ -9,7 +9,6 @@ import * as ts from 'typescript'; import {createLanguageService} from '../src/language_service'; -import {Completions, LanguageService} from '../src/types'; import {TypeScriptServiceHost} from '../src/typescript_host'; import {toh} from './test_data'; diff --git a/packages/language-service/test/reflector_host_spec.ts b/packages/language-service/test/reflector_host_spec.ts index 8e706dc3eb..59eb3918ba 100644 --- a/packages/language-service/test/reflector_host_spec.ts +++ b/packages/language-service/test/reflector_host_spec.ts @@ -18,9 +18,14 @@ describe('reflector_host_spec', () => { // Regression #21811 it('should be able to find angular under windows', () => { const originalJoin = path.join; - let mockHost = new MockTypescriptHost( - ['/app/main.ts', '/app/parsing-cases.ts'], toh, 'app/node_modules', - {...path, join: (...args: string[]) => originalJoin.apply(path, args)}); + const originalPosixJoin = path.posix.join; + let mockHost = + new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh, 'app/node_modules', { + ...path, + join: (...args: string[]) => originalJoin.apply(path, args), + posix: + {...path.posix, join: (...args: string[]) => originalPosixJoin.apply(path, args)} + }); const reflectorHost = new ReflectorHost(() => undefined as any, mockHost, {basePath: '\\app'}); if (process.platform !== 'win32') { diff --git a/packages/language-service/test/template_references_spec.ts b/packages/language-service/test/template_references_spec.ts index 814679a652..1147bc48d1 100644 --- a/packages/language-service/test/template_references_spec.ts +++ b/packages/language-service/test/template_references_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {createLanguageService} from '../src/language_service'; -import {Completions, Diagnostic, Diagnostics, LanguageService} from '../src/types'; +import {LanguageService} from '../src/types'; import {TypeScriptServiceHost} from '../src/typescript_host'; import {toh} from './test_data'; @@ -19,14 +19,12 @@ describe('references', () => { let documentRegistry = ts.createDocumentRegistry(); let mockHost: MockTypescriptHost; let service: ts.LanguageService; - let program: ts.Program; let ngHost: TypeScriptServiceHost; let ngService: LanguageService = createLanguageService(undefined !); beforeEach(() => { mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh); service = ts.createLanguageService(mockHost, documentRegistry); - program = service.getProgram() !; ngHost = new TypeScriptServiceHost(mockHost, service); ngService = createLanguageService(ngHost); ngHost.setSite(ngService); diff --git a/packages/language-service/test/test_data.ts b/packages/language-service/test/test_data.ts index 34931629aa..ebc639f9e2 100644 --- a/packages/language-service/test/test_data.ts +++ b/packages/language-service/test/test_data.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {MockData} from './test_utils'; - export const toh = { 'foo.ts': `export * from './app/app.component.ts';`, app: { diff --git a/packages/language-service/test/test_utils.ts b/packages/language-service/test/test_utils.ts index 350a039084..ea8f67824e 100644 --- a/packages/language-service/test/test_utils.ts +++ b/packages/language-service/test/test_utils.ts @@ -72,13 +72,15 @@ export class MockTypescriptHost implements ts.LanguageServiceHost { private projectVersion = 0; private options: ts.CompilerOptions; private overrideDirectory = new Set(); + private existsCache = new Map(); + private fileCache = new Map(); constructor( private scriptNames: string[], private data: MockData, private node_modules: string = 'node_modules', private myPath: typeof path = path) { const support = setup(); - this.nodeModulesPath = path.join(support.basePath, 'node_modules'); - this.angularPath = path.join(this.nodeModulesPath, '@angular'); + this.nodeModulesPath = path.posix.join(support.basePath, 'node_modules'); + this.angularPath = path.posix.join(this.nodeModulesPath, '@angular'); this.options = { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, @@ -146,7 +148,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost { } else if (effectiveName == '/' + this.node_modules) { return true; } else { - return fs.existsSync(effectiveName); + return this.pathExists(effectiveName); } } @@ -186,14 +188,19 @@ export class MockTypescriptHost implements ts.LanguageServiceHost { cacheUsed.add(fileName); return undefined; } - let effectiveName = this.getEffectiveName(fileName); - if (effectiveName === fileName) + + const effectiveName = this.getEffectiveName(fileName); + if (effectiveName === fileName) { return open(fileName, this.data); - else if ( + } else if ( !fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) && !fileName.match(tsxfile)) { - if (fs.existsSync(effectiveName)) { - return fs.readFileSync(effectiveName, 'utf8'); + if (this.fileCache.has(effectiveName)) { + return this.fileCache.get(effectiveName); + } else if (this.pathExists(effectiveName)) { + const content = fs.readFileSync(effectiveName, 'utf8'); + this.fileCache.set(effectiveName, content); + return content; } else { missingCache.set(fileName, true); reportedMissing.add(fileName); @@ -203,19 +210,29 @@ export class MockTypescriptHost implements ts.LanguageServiceHost { } } + private pathExists(path: string): boolean { + if (this.existsCache.has(path)) { + return this.existsCache.get(path) !; + } + + const exists = fs.existsSync(path); + this.existsCache.set(path, exists); + return exists; + } + private getEffectiveName(name: string): string { const node_modules = this.node_modules; const at_angular = '/@angular'; if (name.startsWith('/' + node_modules)) { if (this.nodeModulesPath && !name.startsWith('/' + node_modules + at_angular)) { - let result = this.myPath.join(this.nodeModulesPath, name.substr(node_modules.length + 1)); - if (!name.match(rxjsts)) - if (fs.existsSync(result)) { - return result; - } + const result = + this.myPath.posix.join(this.nodeModulesPath, name.substr(node_modules.length + 1)); + if (!name.match(rxjsts) && this.pathExists(result)) { + return result; + } } if (this.angularPath && name.startsWith('/' + node_modules + at_angular)) { - return this.myPath.join( + return this.myPath.posix.join( this.angularPath, name.substr(node_modules.length + at_angular.length + 1)); } } @@ -279,8 +296,6 @@ function getLocationMarkers(value: string): {[name: string]: number} { } const referenceMarker = /«(((\w|\-)+)|([^ᐱ]*ᐱ(\w+)ᐱ.[^»]*))»/g; -const definitionMarkerGroup = 1; -const nameMarkerGroup = 2; export type ReferenceMarkers = { [name: string]: Span[] diff --git a/packages/language-service/test/ts_plugin_spec.ts b/packages/language-service/test/ts_plugin_spec.ts index 5948391008..5ccbbcd67c 100644 --- a/packages/language-service/test/ts_plugin_spec.ts +++ b/packages/language-service/test/ts_plugin_spec.ts @@ -212,12 +212,6 @@ describe('plugin', () => { locationMarker, plugin.getCompletionsAtPosition(fileName, location, undefined) !, ...names); } - function expectEmpty(fileName: string, locationMarker: string) { - const location = getMarkerLocation(fileName, locationMarker); - expect(plugin.getCompletionsAtPosition(fileName, location, undefined) !.entries || []).toEqual([ - ]); - } - function expectSemanticError(fileName: string, locationMarker: string, message: string) { const start = getMarkerLocation(fileName, locationMarker); const end = getMarkerLocation(fileName, locationMarker + '-end'); diff --git a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts index ead5fa17b2..316152f7b9 100644 --- a/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts +++ b/packages/platform-browser-dynamic/test/resource_loader/resource_loader_cache_spec.ts @@ -21,17 +21,6 @@ if (isBrowser) { setTemplateCache({'test.html': '
        Hello
        '}); return new CachedResourceLoader(); } - beforeEach(fakeAsync(() => { - TestBed.configureCompiler({ - providers: [ - {provide: UrlResolver, useClass: TestUrlResolver, deps: []}, - {provide: ResourceLoader, useFactory: createCachedResourceLoader, deps: []} - ] - }); - - TestBed.configureTestingModule({declarations: [TestComponent]}); - TestBed.compileComponents(); - })); it('should throw exception if $templateCache is not found', () => { setTemplateCache(null); @@ -41,20 +30,25 @@ if (isBrowser) { }); it('should resolve the Promise with the cached file content on success', async(() => { - setTemplateCache({'test.html': '
        Hello
        '}); - resourceLoader = new CachedResourceLoader(); + resourceLoader = createCachedResourceLoader(); resourceLoader.get('test.html').then((text) => { expect(text).toBe('
        Hello
        '); }); })); it('should reject the Promise on failure', async(() => { - resourceLoader = new CachedResourceLoader(); - resourceLoader.get('unknown.html') - .then((text) => { throw new Error('Not expected to succeed.'); }) - .catch((error) => {/** success */}); + resourceLoader = createCachedResourceLoader(); + resourceLoader.get('unknown.html').then(() => { + throw new Error('Not expected to succeed.'); + }, () => {/* success */}); })); it('should allow fakeAsync Tests to load components with templateUrl synchronously', fakeAsync(() => { + TestBed.configureCompiler({ + providers: [ + {provide: UrlResolver, useClass: TestUrlResolver, deps: []}, + {provide: ResourceLoader, useFactory: createCachedResourceLoader, deps: []} + ] + }); TestBed.configureTestingModule({declarations: [TestComponent]}); TestBed.compileComponents(); tick(); diff --git a/packages/platform-browser/animations/test/animation_renderer_spec.ts b/packages/platform-browser/animations/test/animation_renderer_spec.ts index 83f0b08d4f..67bdd84724 100644 --- a/packages/platform-browser/animations/test/animation_renderer_spec.ts +++ b/packages/platform-browser/animations/test/animation_renderer_spec.ts @@ -168,7 +168,7 @@ import {el} from '../../testing/src/browser_util'; }) class Cmp { exp: any; - @ViewChild('elm') public element: any; + @ViewChild('elm', {static: false}) public element: any; } TestBed.configureTestingModule({ @@ -213,11 +213,11 @@ import {el} from '../../testing/src/browser_util'; exp2: any = true; exp3: any = true; - @ViewChild('elm1') public elm1: any; + @ViewChild('elm1', {static: false}) public elm1: any; - @ViewChild('elm2') public elm2: any; + @ViewChild('elm2', {static: false}) public elm2: any; - @ViewChild('elm3') public elm3: any; + @ViewChild('elm3', {static: false}) public elm3: any; } TestBed.configureTestingModule({ diff --git a/packages/platform-browser/animations/test/browser_animation_builder_spec.ts b/packages/platform-browser/animations/test/browser_animation_builder_spec.ts index 38535fcda5..72cbfb10cd 100644 --- a/packages/platform-browser/animations/test/browser_animation_builder_spec.ts +++ b/packages/platform-browser/animations/test/browser_animation_builder_spec.ts @@ -51,7 +51,7 @@ import {el} from '../../testing/src/browser_util'; template: '...', }) class Cmp { - @ViewChild('target') public target: any; + @ViewChild('target', {static: false}) public target: any; constructor(public builder: AnimationBuilder) {} diff --git a/packages/platform-browser/src/browser/browser_adapter.ts b/packages/platform-browser/src/browser/browser_adapter.ts index dbe7baf085..d40cf891eb 100644 --- a/packages/platform-browser/src/browser/browser_adapter.ts +++ b/packages/platform-browser/src/browser/browser_adapter.ts @@ -63,13 +63,15 @@ const _chromeNumKeyPadMap = { '\x90': 'NumLock' }; -let nodeContains: (a: any, b: any) => boolean; +const nodeContains: (a: any, b: any) => boolean = (() => { + if (global['Node']) { + return global['Node'].prototype.contains || function(node: any) { + return !!(this.compareDocumentPosition(node) & 16); + }; + } -if (global['Node']) { - nodeContains = global['Node'].prototype.contains || function(node) { - return !!(this.compareDocumentPosition(node) & 16); - }; -} + return undefined as any; +})(); /** * A `DomAdapter` powered by full browser DOM APIs. diff --git a/packages/platform-browser/src/dom/debug/ng_probe.ts b/packages/platform-browser/src/dom/debug/ng_probe.ts index d033a68145..2ed9656c3b 100644 --- a/packages/platform-browser/src/dom/debug/ng_probe.ts +++ b/packages/platform-browser/src/dom/debug/ng_probe.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import * as core from '@angular/core'; +import {APP_INITIALIZER, ApplicationRef, DebugNode, NgProbeToken, NgZone, Optional, Provider, getDebugNode} from '@angular/core'; + import {exportNgVar} from '../util'; -const CORE_TOKENS = { - 'ApplicationRef': core.ApplicationRef, - 'NgZone': core.NgZone, -}; +const CORE_TOKENS = (() => ({ + 'ApplicationRef': ApplicationRef, + 'NgZone': NgZone, + }))(); const INSPECT_GLOBAL_NAME = 'probe'; const CORE_TOKENS_GLOBAL_NAME = 'coreTokens'; @@ -22,17 +23,17 @@ const CORE_TOKENS_GLOBAL_NAME = 'coreTokens'; * null if the given native element does not have an Angular view associated * with it. */ -export function inspectNativeElement(element: any): core.DebugNode|null { - return core.getDebugNode(element); +export function inspectNativeElement(element: any): DebugNode|null { + return getDebugNode(element); } -export function _createNgProbe(coreTokens: core.NgProbeToken[]): any { +export function _createNgProbe(coreTokens: NgProbeToken[]): any { exportNgVar(INSPECT_GLOBAL_NAME, inspectNativeElement); exportNgVar(CORE_TOKENS_GLOBAL_NAME, {...CORE_TOKENS, ..._ngProbeTokensToMap(coreTokens || [])}); return () => inspectNativeElement; } -function _ngProbeTokensToMap(tokens: core.NgProbeToken[]): {[name: string]: any} { +function _ngProbeTokensToMap(tokens: NgProbeToken[]): {[name: string]: any} { return tokens.reduce((prev: any, t: any) => (prev[t.name] = t.token, prev), {}); } @@ -48,12 +49,12 @@ export const ELEMENT_PROBE_PROVIDERS__POST_R3__ = []; /** * Providers which support debugging Angular applications (e.g. via `ng.probe`). */ -export const ELEMENT_PROBE_PROVIDERS__PRE_R3__: core.Provider[] = [ +export const ELEMENT_PROBE_PROVIDERS__PRE_R3__: Provider[] = [ { - provide: core.APP_INITIALIZER, + provide: APP_INITIALIZER, useFactory: _createNgProbe, deps: [ - [core.NgProbeToken, new core.Optional()], + [NgProbeToken, new Optional()], ], multi: true, }, diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index 16be147431..2ec54e612e 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -157,7 +157,7 @@ class DefaultDomRenderer2 implements Renderer2 { setAttribute(el: any, name: string, value: string, namespace?: string): void { if (namespace) { - name = `${namespace}:${name}`; + name = namespace + ':' + name; // TODO(benlesh): Ivy may cause issues here because it's passing around // full URIs for namespaces, therefore this lookup will fail. const namespaceUri = NAMESPACE_URIS[namespace]; @@ -231,7 +231,7 @@ class DefaultDomRenderer2 implements Renderer2 { } } -const AT_CHARCODE = '@'.charCodeAt(0); +const AT_CHARCODE = (() => '@'.charCodeAt(0))(); function checkNoSyntheticProp(name: string, nameKind: string) { if (name.charCodeAt(0) === AT_CHARCODE) { throw new Error( diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 9684bcd252..2d3045eb15 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -18,9 +18,8 @@ import {EventManagerPlugin} from './event_manager'; * addEventListener by 3x. */ const __symbol__ = - (typeof Zone !== 'undefined') && (Zone as any)['__symbol__'] || function(v: string): string { - return '__zone_symbol__' + v; - }; + (() => (typeof Zone !== 'undefined') && (Zone as any)['__symbol__'] || + function(v: string): string { return '__zone_symbol__' + v; })(); const ADD_EVENT_LISTENER: 'addEventListener' = __symbol__('addEventListener'); const REMOVE_EVENT_LISTENER: 'removeEventListener' = __symbol__('removeEventListener'); @@ -35,13 +34,18 @@ const NATIVE_REMOVE_LISTENER = 'removeEventListener'; const stopSymbol = '__zone_symbol__propagationStopped'; const stopMethodSymbol = '__zone_symbol__stopImmediatePropagation'; -const blackListedEvents: string[] = - (typeof Zone !== 'undefined') && (Zone as any)[__symbol__('BLACK_LISTED_EVENTS')]; -let blackListedMap: {[eventName: string]: string}; -if (blackListedEvents) { - blackListedMap = {}; - blackListedEvents.forEach(eventName => { blackListedMap[eventName] = eventName; }); -} + +const blackListedMap = (() => { + const blackListedEvents: string[] = + (typeof Zone !== 'undefined') && (Zone as any)[__symbol__('BLACK_LISTED_EVENTS')]; + if (blackListedEvents) { + const res: {[eventName: string]: string} = {}; + blackListedEvents.forEach(eventName => { res[eventName] = eventName; }); + return res; + } + return undefined; +})(); + const isBlackListedEvent = function(eventName: string) { if (!blackListedMap) { diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index 1b6de179ec..a641b8ae6e 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -494,27 +494,6 @@ class CompWithUrlTemplate { expect(someModule).toBeAnInstanceOf(SomeModule); }); - obsoleteInIvy(`deprecated method, won't be reimplemented for Render3`) - .it('should keep imported NgModules lazy with deprecatedOverrideProvider', () => { - let someModule: SomeModule|undefined; - - @NgModule() - class SomeModule { - constructor() { someModule = this; } - } - - TestBed.configureTestingModule({ - providers: [ - {provide: 'a', useValue: 'aValue'}, - ], - imports: [SomeModule] - }); - TestBed.deprecatedOverrideProvider('a', {useValue: 'mockValue'}); - - expect(TestBed.get('a')).toBe('mockValue'); - expect(someModule).toBeUndefined(); - }); - describe('injecting eager providers into an eager overwritten provider', () => { @NgModule({ providers: [ diff --git a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts index 08f100e41d..68180cfd67 100644 --- a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts +++ b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts @@ -16,6 +16,7 @@ export {VERSION} from './version'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const platformWorkerAppDynamic = createPlatformFactory(platformCoreDynamic, 'workerAppDynamic', [ diff --git a/packages/platform-webworker-dynamic/src/version.ts b/packages/platform-webworker-dynamic/src/version.ts index 9469ebe5e5..91b55934de 100644 --- a/packages/platform-webworker-dynamic/src/version.ts +++ b/packages/platform-webworker-dynamic/src/version.ts @@ -16,5 +16,6 @@ import {Version} from '@angular/core'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const VERSION = new Version('0.0.0-PLACEHOLDER'); diff --git a/packages/platform-webworker/src/platform-webworker.ts b/packages/platform-webworker/src/platform-webworker.ts index 7dab62437f..ed593eb65b 100644 --- a/packages/platform-webworker/src/platform-webworker.ts +++ b/packages/platform-webworker/src/platform-webworker.ts @@ -24,6 +24,7 @@ export {platformWorkerUi} from './worker_render'; * Bootstraps the worker ui. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export function bootstrapWorkerUi( workerScriptUri: string, customProviders: StaticProvider[] = []): Promise { diff --git a/packages/platform-webworker/src/version.ts b/packages/platform-webworker/src/version.ts index 9469ebe5e5..91b55934de 100644 --- a/packages/platform-webworker/src/version.ts +++ b/packages/platform-webworker/src/version.ts @@ -16,5 +16,6 @@ import {Version} from '@angular/core'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const VERSION = new Version('0.0.0-PLACEHOLDER'); diff --git a/packages/platform-webworker/src/web_workers/shared/client_message_broker.ts b/packages/platform-webworker/src/web_workers/shared/client_message_broker.ts index dc974b65e7..0a79ccd883 100644 --- a/packages/platform-webworker/src/web_workers/shared/client_message_broker.ts +++ b/packages/platform-webworker/src/web_workers/shared/client_message_broker.ts @@ -12,6 +12,7 @@ import {Serializer, SerializerTypes} from './serializer'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ @Injectable() export class ClientMessageBrokerFactory { @@ -39,6 +40,7 @@ interface PromiseCompleter { /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export class ClientMessageBroker { private _pending = new Map(); diff --git a/packages/platform-webworker/src/web_workers/shared/message_bus.ts b/packages/platform-webworker/src/web_workers/shared/message_bus.ts index 51cd04e87f..3afa95f327 100644 --- a/packages/platform-webworker/src/web_workers/shared/message_bus.ts +++ b/packages/platform-webworker/src/web_workers/shared/message_bus.ts @@ -17,6 +17,7 @@ import {EventEmitter, NgZone} from '@angular/core'; * by the corresponding MessageBusSource. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export abstract class MessageBus implements MessageBusSource, MessageBusSink { /** diff --git a/packages/platform-webworker/src/web_workers/shared/serializer.ts b/packages/platform-webworker/src/web_workers/shared/serializer.ts index 2a47cf7458..7b2d16631f 100644 --- a/packages/platform-webworker/src/web_workers/shared/serializer.ts +++ b/packages/platform-webworker/src/web_workers/shared/serializer.ts @@ -12,6 +12,7 @@ import {RenderStore} from './render_store'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const enum SerializerTypes { // RendererType2 diff --git a/packages/platform-webworker/src/web_workers/shared/service_message_broker.ts b/packages/platform-webworker/src/web_workers/shared/service_message_broker.ts index ba97a1124f..f18d7feaa6 100644 --- a/packages/platform-webworker/src/web_workers/shared/service_message_broker.ts +++ b/packages/platform-webworker/src/web_workers/shared/service_message_broker.ts @@ -13,6 +13,7 @@ import {Serializer, SerializerTypes} from '../shared/serializer'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ @Injectable() export class ServiceMessageBrokerFactory { @@ -40,6 +41,7 @@ export class ServiceMessageBrokerFactory { * If that method returns a promise, the UIMessageBroker returns the result to the worker. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export class ServiceMessageBroker { private _sink: EventEmitter; @@ -91,6 +93,7 @@ export class ServiceMessageBroker { /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export interface ReceivedMessage { method: string; diff --git a/packages/platform-webworker/src/web_workers/ui/location_providers.ts b/packages/platform-webworker/src/web_workers/ui/location_providers.ts index ac91a2025d..ac35c15f4a 100644 --- a/packages/platform-webworker/src/web_workers/ui/location_providers.ts +++ b/packages/platform-webworker/src/web_workers/ui/location_providers.ts @@ -22,6 +22,7 @@ import {MessageBasedPlatformLocation} from './platform_location'; * A list of {@link Provider}s. To use the router in a Worker enabled application you must * include these providers when setting up the render thread. * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const WORKER_UI_LOCATION_PROVIDERS = [ {provide: MessageBasedPlatformLocation, deps: [ServiceMessageBrokerFactory, diff --git a/packages/platform-webworker/src/web_workers/worker/location_providers.ts b/packages/platform-webworker/src/web_workers/worker/location_providers.ts index 5438cd1bbe..e61ce5cf5b 100644 --- a/packages/platform-webworker/src/web_workers/worker/location_providers.ts +++ b/packages/platform-webworker/src/web_workers/worker/location_providers.ts @@ -17,6 +17,7 @@ import {WebWorkerPlatformLocation} from './platform_location'; * a worker context. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const WORKER_APP_LOCATION_PROVIDERS = [ {provide: PlatformLocation, useClass: WebWorkerPlatformLocation}, { diff --git a/packages/platform-webworker/src/worker_app.ts b/packages/platform-webworker/src/worker_app.ts index 061ca060ee..987e613586 100644 --- a/packages/platform-webworker/src/worker_app.ts +++ b/packages/platform-webworker/src/worker_app.ts @@ -24,6 +24,7 @@ import {WorkerDomAdapter} from './web_workers/worker/worker_adapter'; /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const platformWorkerApp = createPlatformFactory( platformCore, 'workerApp', [{provide: PLATFORM_ID, useValue: PLATFORM_WORKER_APP_ID}]); @@ -56,6 +57,7 @@ export function setupWebWorker(): void { * The ng module for the worker app side. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ @NgModule({ providers: [ diff --git a/packages/platform-webworker/src/worker_render.ts b/packages/platform-webworker/src/worker_render.ts index f5b3ea5e27..b03dbb7776 100644 --- a/packages/platform-webworker/src/worker_render.ts +++ b/packages/platform-webworker/src/worker_render.ts @@ -26,6 +26,7 @@ import {MessageBasedRenderer2} from './web_workers/ui/renderer'; * and underlying {@link MessageBus} for lower level message passing. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ @Injectable() export class WebWorkerInstance { @@ -43,6 +44,7 @@ export class WebWorkerInstance { /** * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const WORKER_SCRIPT = new InjectionToken('WebWorkerScript'); @@ -51,6 +53,7 @@ export const WORKER_SCRIPT = new InjectionToken('WebWorkerScript'); * created. * * @publicApi + * @deprecated platform-webworker is deprecated in Angular and will be removed in version 10 */ export const WORKER_UI_STARTABLE_MESSAGING_SERVICE = new InjectionToken<({start: () => void})[]>('WorkerRenderStartableMsgService'); diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index e85be8d499..eced49e793 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -626,6 +626,7 @@ export class Router { * way the next navigation will be coming from the current URL in the browser. */ this.rawUrlTree = t.rawUrl; + this.browserUrlTree = t.urlAfterRedirects; t.resolve(null); return EMPTY; } @@ -843,7 +844,14 @@ export class Router { this.routerState.root.component = this.rootComponentType; } - private getTransition(): NavigationTransition { return this.transitions.value; } + private getTransition(): NavigationTransition { + const transition = this.transitions.value; + // This value needs to be set. Other values such as extractedUrl are set on initial navigation + // but the urlAfterRedirects may not get set if we aren't processing the new URL *and* not + // processing the previous URL. + transition.urlAfterRedirects = this.browserUrlTree; + return transition; + } private setTransition(t: Partial): void { this.transitions.next({...this.getTransition(), ...t}); diff --git a/packages/router/src/utils/collection.ts b/packages/router/src/utils/collection.ts index 6834cec4cf..b71c5257f7 100644 --- a/packages/router/src/utils/collection.ts +++ b/packages/router/src/utils/collection.ts @@ -21,9 +21,13 @@ export function shallowEqualArrays(a: any[], b: any[]): boolean { } export function shallowEqual(a: {[x: string]: any}, b: {[x: string]: any}): boolean { - const k1 = Object.keys(a); - const k2 = Object.keys(b); - if (k1.length != k2.length) { + // Casting Object.keys return values to include `undefined` as there are some cases + // in IE 11 where this can happen. Cannot provide a test because the behavior only + // exists in certain circumstances in IE 11, therefore doing this cast ensures the + // logic is correct for when this edge case is hit. + const k1 = Object.keys(a) as string[] | undefined; + const k2 = Object.keys(b) as string[] | undefined; + if (!k1 || !k2 || k1.length != k2.length) { return false; } let key: string; diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index e5e85e84e1..3766ef7065 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -218,6 +218,39 @@ describe('Integration', () => { }))); }); + + /** + * get/setTransition are private APIs. This test is needed though to guarantee the correct + * values are being used. Related to https://github.com/angular/angular/issues/30340 where + * stale transition data was being used when kicking off a new navigation. + */ + describe('get/setTransition', () => { + it('should provide the most recent NavigationTransition', + fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, {path: 'a', component: SimpleCmp}, + {path: 'b', component: SimpleCmp} + ]); + + const fixture = createRoot(router, RootCmp); + + const initialTransition = (router as any).getTransition(); + + // Confirm initial value + expect(initialTransition.urlAfterRedirects.toString()).toBe('/'); + + + router.navigateByUrl('/a', {replaceUrl: true}); + + tick(); + + // After a navigation, we should see the URL after redirect + const nextTransition = (router as any).getTransition(); + // Confirm initial value + expect(nextTransition.urlAfterRedirects.toString()).toBe('/a'); + }))); + }); + describe('navigation warning', () => { let warnings: string[] = []; @@ -662,6 +695,33 @@ describe('Integration', () => { expect(location.path()).toEqual('/login'); }))); + it('should set browserUrlTree with urlUpdateStrategy="eagar" and false `shouldProcessUrl`', + fakeAsync(inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); + + router.urlUpdateStrategy = 'eager'; + + router.resetConfig([ + {path: 'team/:id', component: SimpleCmp}, + {path: 'login', component: AbsoluteSimpleLinkCmp} + ]); + + router.navigateByUrl('/team/22'); + advance(fixture, 1); + + expect((router as any).browserUrlTree.toString()).toBe('/team/22'); + + // Force to not process URL changes + router.urlHandlingStrategy.shouldProcessUrl = (url: UrlTree) => false; + + router.navigateByUrl('/login'); + advance(fixture, 1); + + // Do not change locations + expect((router as any).browserUrlTree.toString()).toBe('/team/22'); + }))); + it('should eagerly update URL after redirects are applied with urlUpdateStrategy="eagar"', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { const fixture = TestBed.createComponent(RootCmp); diff --git a/packages/router/test/regression_integration.spec.ts b/packages/router/test/regression_integration.spec.ts index 686a79b55e..e5e11d7ae3 100644 --- a/packages/router/test/regression_integration.spec.ts +++ b/packages/router/test/regression_integration.spec.ts @@ -73,9 +73,10 @@ describe('Integration', () => { }) class ComponentWithRouterLink { // TODO(issue/24571): remove '!'. - @ViewChild(TemplateRef) templateRef !: TemplateRef; + @ViewChild(TemplateRef, {static: true}) templateRef !: TemplateRef; // TODO(issue/24571): remove '!'. - @ViewChild('container', {read: ViewContainerRef}) container !: ViewContainerRef; + @ViewChild('container', {read: ViewContainerRef, static: true}) + container !: ViewContainerRef; addLink() { this.container.createEmbeddedView(this.templateRef, {$implicit: '/simple'}); diff --git a/protractor-perf.conf.js b/protractor-perf.conf.js index 2839079890..748ea12c2b 100644 --- a/protractor-perf.conf.js +++ b/protractor-perf.conf.js @@ -34,7 +34,7 @@ exports.config = { framework: 'jasmine2', jasmineNodeOpts: { showColors: true, - defaultTimeoutInterval: 60000, + defaultTimeoutInterval: 90000, print: function(msg) { console.log(msg); }, }, useAllAngular2AppRoots: true diff --git a/scripts/github/merge-pr b/scripts/github/merge-pr index 1bd4ba756c..4ad7a09aa1 100755 --- a/scripts/github/merge-pr +++ b/scripts/github/merge-pr @@ -131,8 +131,8 @@ CHERRY_PICK_PR="git cherry-pick merge_pr_base..merge_pr" # # This check is used to enforce that we don't merge PRs that have not been rebased recently and could result in merging # of non-approved or otherwise bad changes. -REQUIRED_BASE_SHA_MASTER="3fba6eff79a9b50909199eaa4ebf754c1c4adba6" # pullapprove => CODEOWNERS migration -REQUIRED_BASE_SHA_PATCH="e3853e842ea5c10fafbc310a76a4a7f47ed8c65e" # pullapprove => CODEOWNERS migration +REQUIRED_BASE_SHA_MASTER="a03a9236f2aed5d00012d25f032aa43a046d91da" # pullapprove => CODEOWNERS migration +REQUIRED_BASE_SHA_PATCH="a03a9236f2aed5d00012d25f032aa43a046d91da" # pullapprove => CODEOWNERS migration if [[ $MERGE_MASTER == 1 ]]; then REQUIRED_BASE_SHA="$REQUIRED_BASE_SHA_MASTER" # check patch only if patch-only PR diff --git a/tools/gulp-tasks/cldr/extract.js b/tools/gulp-tasks/cldr/extract.js index 3c0e0ce16c..1a92bc8fd2 100644 --- a/tools/gulp-tasks/cldr/extract.js +++ b/tools/gulp-tasks/cldr/extract.js @@ -14,11 +14,14 @@ const cldr = require('cldr'); // used to extract all other cldr data const cldrJs = require('cldrjs'); -const PACKAGE_FOLDER = 'packages/common'; -const I18N_FOLDER = `${PACKAGE_FOLDER}/src/i18n`; -const I18N_DATA_FOLDER = `${PACKAGE_FOLDER}/locales`; +const COMMON_PACKAGE = 'packages/common'; +const CORE_PACKAGE = 'packages/core'; +const I18N_FOLDER = `${COMMON_PACKAGE}/src/i18n`; +const I18N_CORE_FOLDER = `${CORE_PACKAGE}/src/i18n`; +const I18N_DATA_FOLDER = `${COMMON_PACKAGE}/locales`; const I18N_DATA_EXTRA_FOLDER = `${I18N_DATA_FOLDER}/extra`; const RELATIVE_I18N_FOLDER = path.resolve(__dirname, `../../../${I18N_FOLDER}`); +const RELATIVE_I18N_CORE_FOLDER = path.resolve(__dirname, `../../../${I18N_CORE_FOLDER}`); const RELATIVE_I18N_DATA_FOLDER = path.resolve(__dirname, `../../../${I18N_DATA_FOLDER}`); const RELATIVE_I18N_DATA_EXTRA_FOLDER = path.resolve(__dirname, `../../../${I18N_DATA_EXTRA_FOLDER}`); const DEFAULT_RULE = 'function anonymous(n\n/*``*/) {\nreturn"other"\n}'; @@ -60,9 +63,9 @@ module.exports = (gulp, done) => { const baseCurrencies = generateBaseCurrencies(new cldrJs('en')); // additional "en" file that will be included in common - console.log(`Writing file ${I18N_FOLDER}/locale_en.ts`); + console.log(`Writing file ${I18N_CORE_FOLDER}/locale_en.ts`); const localeEnFile = generateLocale('en', new cldrJs('en'), baseCurrencies); - fs.writeFileSync(`${RELATIVE_I18N_FOLDER}/locale_en.ts`, localeEnFile); + fs.writeFileSync(`${RELATIVE_I18N_CORE_FOLDER}/locale_en.ts`, localeEnFile); LOCALES.forEach((locale, index) => { const localeData = new cldrJs(locale); @@ -82,7 +85,7 @@ module.exports = (gulp, done) => { .src([ `${I18N_DATA_FOLDER}/**/*.ts`, `${I18N_FOLDER}/currencies.ts`, - `${I18N_FOLDER}/locale_en.ts` + `${I18N_CORE_FOLDER}/locale_en.ts` ], {base: '.'}) .pipe(format.format('file', clangFormat)) .pipe(gulp.dest('.')); diff --git a/tools/postinstall-patches.js b/tools/postinstall-patches.js index 82e9eb5a29..04c8e9171f 100644 --- a/tools/postinstall-patches.js +++ b/tools/postinstall-patches.js @@ -27,21 +27,28 @@ const log = console.log; // COMMENTED OUT BECAUSE WE CURRENTLY REQUIRE NO PATCHES // UNCOMMENT TO REENABLE PATCHING AND LOG OUTPUT // -// log('===== about to run the postinstall-patches.js script ====='); -// // fail on first error -// set('-e'); -// // print commands as being executed -// set('-v'); -// // jump to project root -// cd(path.join(__dirname, '../')); +log('===== about to run the postinstall-patches.js script ====='); +// fail on first error +set('-e'); +// print commands as being executed +set('-v'); +// jump to project root +cd(path.join(__dirname, '../')); -// /* EXAMPLE PATCH: -// // https://github.com/ReactiveX/rxjs/pull/3302 -// // make node_modules/rxjs compilable with Typescript 2.7 -// // remove when we update to rxjs v6 -// log('\n# patch: reactivex/rxjs#3302 make node_modules/rxjs compilable with Typescript 2.7'); -// sed('-i', '(\'response\' in xhr)', '(\'response\' in (xhr as any))', -// 'node_modules/rxjs/src/observable/dom/AjaxObservable.ts'); -// */ +/* EXAMPLE PATCH: +// https://github.com/ReactiveX/rxjs/pull/3302 +// make node_modules/rxjs compilable with Typescript 2.7 +// remove when we update to rxjs v6 +log('\n# patch: reactivex/rxjs#3302 make node_modules/rxjs compilable with Typescript 2.7'); +sed('-i', '(\'response\' in xhr)', '(\'response\' in (xhr as any))', + 'node_modules/rxjs/src/observable/dom/AjaxObservable.ts'); +*/ -// log('===== finished running the postinstall-patches.js script ====='); +// make chrome 74 work on OSX with karma under bazel +// remove when we update to the next @bazel/karma release +log('\n# patch: @bazel/karma 0.29.0 to disable chrome sandbox for OSX'); +sed('-i', 'process.platform !== \'linux\'', + 'process.platform !== \'linux\' && process.platform !== \'darwin\'', + 'node_modules/@bazel/karma/karma.conf.js'); + +log('===== finished running the postinstall-patches.js script ====='); diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 50eee2b32f..2606601215 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -103,7 +103,7 @@ export declare function getLocaleNumberFormat(locale: string, type: NumberFormat export declare function getLocaleNumberSymbol(locale: string, symbol: NumberSymbol): string; -export declare function getLocalePluralCase(locale: string): (value: number) => Plural; +export declare const getLocalePluralCase: (locale: string) => ((value: number) => Plural); export declare function getLocaleTimeFormat(locale: string, width: FormatWidth): string; @@ -263,7 +263,7 @@ export declare class NgIf { ngIfElse: TemplateRef | null; ngIfThen: TemplateRef | null; constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef); - static ngTemplateGuard_ngIf(dir: NgIf, expr: E): expr is NonNullable; + static ngTemplateGuard_ngIf: 'binding'; } export declare class NgIfContext { @@ -410,7 +410,10 @@ export interface PopStateEvent { export declare function registerLocaleData(data: any, localeId?: string | any, extraData?: any): void; export declare class SlicePipe implements PipeTransform { - transform(value: any, start: number, end?: number): any; + transform(value: ReadonlyArray, start: number, end?: number): Array; + transform(value: string, start: number, end?: number): string; + transform(value: null, start: number, end?: number): null; + transform(value: undefined, start: number, end?: number): undefined; } export declare type Time = { diff --git a/tools/public_api_guard/common/upgrade.d.ts b/tools/public_api_guard/common/upgrade.d.ts index 3f50d4970f..4ea123fffa 100644 --- a/tools/public_api_guard/common/upgrade.d.ts +++ b/tools/public_api_guard/common/upgrade.d.ts @@ -6,6 +6,7 @@ export declare class $locationShim { hash(hash: string | number | null): this; hash(): string; host(): string; + onChange(fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void, err?: (e: Error) => void): void; path(): string; path(path: string | number | null): this; port(): number | null; @@ -34,7 +35,7 @@ export declare class $locationShimProvider { } export declare class AngularJSUrlCodec implements UrlCodec { - areEqual(a: string, b: string): boolean; + areEqual(valA: string, valB: string): boolean; decodeHash(hash: string): string; decodePath(path: string, html5Mode?: boolean): string; decodeSearch(search: string): { @@ -76,7 +77,7 @@ export declare class LocationUpgradeModule { } export declare abstract class UrlCodec { - abstract areEqual(a: string, b: string): boolean; + abstract areEqual(valA: string, valB: string): boolean; abstract decodeHash(hash: string): string; abstract decodePath(path: string): string; abstract decodeSearch(search: string): { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index ba7e436cf3..15d50079a4 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -168,13 +168,13 @@ export interface ConstructorSansProvider { export declare type ContentChild = Query; export interface ContentChildDecorator { - (selector: Type | Function | string, opts?: { + (selector: Type | Function | string, opts: { read?: any; - static?: boolean; + static: boolean; }): any; - new (selector: Type | Function | string, opts?: { + new (selector: Type | Function | string, opts: { read?: any; - static?: boolean; + static: boolean; }): ContentChild; } @@ -661,6 +661,26 @@ export interface OutputDecorator { export declare function ɵɵallocHostVars(count: number): void; +export declare function ɵɵattribute(name: string, value: any, sanitizer?: SanitizerFn | null, namespace?: string): void; + +export declare function ɵɵattributeInterpolate1(attrName: string, prefix: string, v0: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate2(attrName: string, prefix: string, v0: any, i0: string, v1: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate3(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate4(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate5(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate6(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate7(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolate8(attrName: string, prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + +export declare function ɵɵattributeInterpolateV(attrName: string, values: any[], sanitizer?: SanitizerFn, namespace?: string): TsickleIssue1009; + export interface ɵɵBaseDef { contentQueries: ContentQueriesFunction | null; /** @deprecated */ readonly declaredInputs: { @@ -692,8 +712,6 @@ export declare type ɵɵComponentDefWithMeta(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): void; -export declare function ɵɵcomponentHostSyntheticProperty(index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): void; - export declare function ɵɵcontainer(index: number): void; export declare function ɵɵcontainerRefreshEnd(): void; @@ -777,6 +795,7 @@ export declare function ɵɵdefineNgModule(def: { imports?: Type[] | (() => Type[]); exports?: Type[] | (() => Type[]); schemas?: SchemaMetadata[] | null; + id?: string | null; }): never; export declare function ɵɵdefinePipe(pipeDef: { @@ -919,7 +938,7 @@ export declare type ɵɵPipeDefWithMeta = PipeDef; export declare function ɵɵprojection(nodeIndex: number, selectorIndex?: number, attrs?: TAttributes): void; -export declare function ɵɵprojectionDef(selectors?: CssSelectorList[]): void; +export declare function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void; export declare function ɵɵproperty(propName: string, value: T, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009; @@ -1036,6 +1055,28 @@ export declare function ɵɵtext(index: number, value?: any): void; export declare function ɵɵtextBinding(index: number, value: T | NO_CHANGE): void; +export declare function ɵɵtextInterpolate(v0: any): TsickleIssue1009; + +export declare function ɵɵtextInterpolate1(prefix: string, v0: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate2(prefix: string, v0: any, i0: string, v1: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate3(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate4(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate5(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate6(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate7(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolate8(prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, suffix: string): TsickleIssue1009; + +export declare function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009; + +export declare function ɵɵupdateSyntheticHostBinding(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): void; + export declare function ɵɵviewQuery(predicate: Type | string[], descend: boolean, read: any): QueryList; export declare const PACKAGE_ROOT_URL: InjectionToken; @@ -1083,7 +1124,7 @@ export interface Query { isViewQuery: boolean; read: any; selector: any; - static?: boolean; + static: boolean; } export declare abstract class Query { @@ -1095,6 +1136,7 @@ export declare class QueryList { readonly first: T; readonly last: T; readonly length: number; + constructor(); destroy(): void; filter(fn: (item: T, index: number, array: T[]) => boolean): T[]; find(fn: (item: T, index: number, array: T[]) => boolean): T | undefined; @@ -1361,13 +1403,13 @@ export declare const VERSION: Version; export declare type ViewChild = Query; export interface ViewChildDecorator { - (selector: Type | Function | string, opts?: { + (selector: Type | Function | string, opts: { read?: any; - static?: boolean; + static: boolean; }): any; - new (selector: Type | Function | string, opts?: { + new (selector: Type | Function | string, opts: { read?: any; - static?: boolean; + static: boolean; }): ViewChild; } @@ -1419,14 +1461,19 @@ export declare class WrappedValue { static wrap(value: any): WrappedValue; } +/** @deprecated */ export declare const wtfCreateScope: (signature: string, flags?: any) => WtfScopeFn; +/** @deprecated */ export declare const wtfEndTimeRange: (range: any) => void; +/** @deprecated */ export declare const wtfLeave: (scope: any, returnValue?: T) => T; +/** @deprecated */ export interface WtfScopeFn { (arg0?: any, arg1?: any): any; } +/** @deprecated */ export declare const wtfStartTimeRange: (rangeType: string, action: string) => any; diff --git a/tools/public_api_guard/core/testing.d.ts b/tools/public_api_guard/core/testing.d.ts index 6cf7bd65dd..11e0cb85b5 100644 --- a/tools/public_api_guard/core/testing.d.ts +++ b/tools/public_api_guard/core/testing.d.ts @@ -57,37 +57,25 @@ export interface TestBed { }): void; configureTestingModule(moduleDef: TestModuleMetadata): void; createComponent(component: Type): ComponentFixture; - deprecatedOverrideProvider(token: any, provider: { - useFactory?: Function; - useValue?: any; - deps?: any[]; - }): void; - deprecatedOverrideProvider(token: any, provider: { - useValue: any; - }): void; - /** @deprecated */ deprecatedOverrideProvider(token: any, provider: { - useFactory: Function; - deps: any[]; - }): void; execute(tokens: any[], fn: Function, context?: any): any; get(token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; - /** @deprecated */ get(token: any, notFoundValue?: any): any; + get(token: any, notFoundValue?: any): any; initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): void; overrideComponent(component: Type, override: MetadataOverride): void; overrideDirective(directive: Type, override: MetadataOverride): void; overrideModule(ngModule: Type, override: MetadataOverride): void; overridePipe(pipe: Type, override: MetadataOverride): void; overrideProvider(token: any, provider: { - useFactory?: Function; - useValue?: any; - deps?: any[]; + useFactory: Function; + deps: any[]; }): void; overrideProvider(token: any, provider: { useValue: any; }): void; overrideProvider(token: any, provider: { - useFactory: Function; - deps: any[]; + useFactory?: Function; + useValue?: any; + deps?: any[]; }): void; overrideTemplateUsingTestingModule(component: Type, template: string): void; resetTestEnvironment(): void; @@ -105,25 +93,17 @@ export interface TestBedStatic { }): TestBedStatic; configureTestingModule(moduleDef: TestModuleMetadata): TestBedStatic; createComponent(component: Type): ComponentFixture; - deprecatedOverrideProvider(token: any, provider: { - useFactory?: Function; - useValue?: any; - deps?: any[]; - }): TestBedStatic; - deprecatedOverrideProvider(token: any, provider: { - useValue: any; - }): void; - /** @deprecated */ deprecatedOverrideProvider(token: any, provider: { - useFactory: Function; - deps: any[]; - }): void; - /** @deprecated */ get(token: any, notFoundValue?: any): any; get(token: Type | InjectionToken, notFoundValue?: T, flags?: InjectFlags): any; + /** @deprecated */ get(token: any, notFoundValue?: any): any; initTestEnvironment(ngModule: Type | Type[], platform: PlatformRef, aotSummaries?: () => any[]): TestBed; overrideComponent(component: Type, override: MetadataOverride): TestBedStatic; overrideDirective(directive: Type, override: MetadataOverride): TestBedStatic; overrideModule(ngModule: Type, override: MetadataOverride): TestBedStatic; overridePipe(pipe: Type, override: MetadataOverride): TestBedStatic; + overrideProvider(token: any, provider: { + useFactory: Function; + deps: any[]; + }): TestBedStatic; overrideProvider(token: any, provider: { useValue: any; }): TestBedStatic; @@ -132,10 +112,6 @@ export interface TestBedStatic { useValue?: any; deps?: any[]; }): TestBedStatic; - overrideProvider(token: any, provider: { - useFactory: Function; - deps: any[]; - }): TestBedStatic; overrideTemplate(component: Type, template: string): TestBedStatic; overrideTemplateUsingTestingModule(component: Type, template: string): TestBedStatic; resetTestEnvironment(): void; diff --git a/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts b/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts index 8b8e2110a5..f2abbd6e3b 100644 --- a/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts +++ b/tools/public_api_guard/platform-webworker-dynamic/platform-webworker-dynamic.d.ts @@ -1,3 +1,5 @@ +/** @deprecated */ export declare const platformWorkerAppDynamic: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; +/** @deprecated */ export declare const VERSION: Version; diff --git a/tools/public_api_guard/platform-webworker/platform-webworker.d.ts b/tools/public_api_guard/platform-webworker/platform-webworker.d.ts index d9d000f23b..8293c24d7a 100644 --- a/tools/public_api_guard/platform-webworker/platform-webworker.d.ts +++ b/tools/public_api_guard/platform-webworker/platform-webworker.d.ts @@ -1,9 +1,12 @@ +/** @deprecated */ export declare function bootstrapWorkerUi(workerScriptUri: string, customProviders?: StaticProvider[]): Promise; +/** @deprecated */ export declare class ClientMessageBroker { runOnService(args: UiArguments, returnType: Type | SerializerTypes | null): Promise | null; } +/** @deprecated */ export declare class ClientMessageBrokerFactory { createMessageBroker(channel: string, runInZone?: boolean): ClientMessageBroker; } @@ -14,6 +17,7 @@ export declare class FnArg { constructor(value: any, type?: Type | SerializerTypes); } +/** @deprecated */ export declare abstract class MessageBus implements MessageBusSource, MessageBusSink { abstract attachToZone(zone: NgZone): void; abstract from(channel: string): EventEmitter; @@ -33,10 +37,12 @@ export interface MessageBusSource { initChannel(channel: string, runInZone: boolean): void; } +/** @deprecated */ export declare const platformWorkerApp: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; export declare const platformWorkerUi: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; +/** @deprecated */ export interface ReceivedMessage { args: any[]; id: string; @@ -44,16 +50,19 @@ export interface ReceivedMessage { type: string; } +/** @deprecated */ export declare const enum SerializerTypes { RENDERER_TYPE_2 = 0, PRIMITIVE = 1, RENDER_STORE_OBJECT = 2 } +/** @deprecated */ export declare class ServiceMessageBroker { registerMethod(methodName: string, signature: Array | SerializerTypes> | null, method: (..._: any[]) => Promise | void, returnType?: Type | SerializerTypes): void; } +/** @deprecated */ export declare class ServiceMessageBrokerFactory { createMessageBroker(channel: string, runInZone?: boolean): ServiceMessageBroker; } @@ -64,8 +73,10 @@ export declare class UiArguments { constructor(method: string, args?: FnArg[] | undefined); } +/** @deprecated */ export declare const VERSION: Version; +/** @deprecated */ export declare const WORKER_APP_LOCATION_PROVIDERS: ({ provide: typeof PlatformLocation; useClass: typeof WebWorkerPlatformLocation; @@ -86,7 +97,9 @@ export declare const WORKER_APP_LOCATION_PROVIDERS: ({ multi?: undefined; })[]; +/** @deprecated */ export declare const WORKER_UI_LOCATION_PROVIDERS: StaticProvider[]; +/** @deprecated */ export declare class WorkerAppModule { } diff --git a/tools/size-tracking/BUILD.bazel b/tools/size-tracking/BUILD.bazel index 7a040f4832..b01bed4829 100644 --- a/tools/size-tracking/BUILD.bazel +++ b/tools/size-tracking/BUILD.bazel @@ -12,6 +12,8 @@ ts_library( deps = [ "@npm//@types/node", "@npm//@types/source-map", + "@npm//chalk", + "@npm//source-map", ], ) @@ -21,7 +23,7 @@ ts_library( srcs = glob(["**/*_spec.ts"]), deps = [ ":size-tracking", - "@npm//@types/source-map", + "@npm//source-map", ], ) diff --git a/tools/ts-api-guardian/BUILD.bazel b/tools/ts-api-guardian/BUILD.bazel index b830f9842f..cdf3db75d0 100644 --- a/tools/ts-api-guardian/BUILD.bazel +++ b/tools/ts-api-guardian/BUILD.bazel @@ -1,9 +1,6 @@ # BEGIN-INTERNAL -load( - "@build_bazel_rules_nodejs//:defs.bzl", - "jasmine_node_test", - "npm_package", -) +load("@build_bazel_rules_nodejs//:defs.bzl", "npm_package") +load("@npm_bazel_jasmine//:index.bzl", "jasmine_node_test") load("@npm_bazel_typescript//:index.bzl", "ts_library") ts_library( diff --git a/tools/ts-api-guardian/README.md b/tools/ts-api-guardian/README.md index 19f343b4e7..296d4e1e6f 100644 --- a/tools/ts-api-guardian/README.md +++ b/tools/ts-api-guardian/README.md @@ -30,7 +30,7 @@ $ yarn bazel test //tools/ts-api-guardian:all Publish to NPM: ```sh -$ npm whoami # should be logged in as angular +$ yarn bazel run @nodejs//:npm whoami # should be logged in as angular $ grep version tools/ts-api-guardian/package.json # advance as needed $ yarn bazel run //tools/ts-api-guardian:ts-api-guardian.publish ``` diff --git a/tools/ts-api-guardian/index.bzl b/tools/ts-api-guardian/index.bzl index a33863f7f3..da378c2882 100644 --- a/tools/ts-api-guardian/index.bzl +++ b/tools/ts-api-guardian/index.bzl @@ -24,8 +24,6 @@ def ts_api_guardian_test( golden, actual, data = [], - # Match one, but not two ɵ characters. Ivy instructions are currently prefixed with ɵɵ and - # should appear in ts_api_guardian tests. strip_export_pattern = ["^__", "^ɵ[^ɵ]"], allow_module_identifiers = COMMON_MODULE_IDENTIFIERS, use_angular_tag_rules = True, diff --git a/tools/ts-api-guardian/package.json b/tools/ts-api-guardian/package.json index 79006d7179..9ea00518b4 100644 --- a/tools/ts-api-guardian/package.json +++ b/tools/ts-api-guardian/package.json @@ -1,12 +1,12 @@ { "name": "ts-api-guardian", - "version": "0.4.1", + "version": "0.4.4", "description": "Guards the API of TypeScript libraries!", "main": "lib/main.js", "typings": "lib/main.d.ts", "bazelWorkspaces": { "npm_ts_api_guardian": { - "version": "0.4.1", + "version": "0.4.4", "rootPath": "." } }, diff --git a/tslint.json b/tslint.json index d12df37dde..91b52d2fe3 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,8 @@ "rulesDirectory": [ "dist/tools/tslint", "node_modules/vrsource-tslint-rules/rules", - "node_modules/tslint-eslint-rules/dist/rules" + "node_modules/tslint-eslint-rules/dist/rules", + "node_modules/tslint-no-toplevel-property-access/rules" ], "rules": { "file-header": [ @@ -18,6 +19,18 @@ "no-jasmine-focus": true, "no-var-keyword": true, "require-internal-with-underscore": true, + "no-toplevel-property-access": [ + true, + "packages/animations/src/", + "packages/animations/browser/", + "packages/common/src/", + "packages/core/src/", + "packages/elements/src/", + "packages/forms/src/", + "packages/http/src/", + "packages/platform-browser/src/", + "packages/router/src/" + ], "semicolon": [ true ], @@ -56,4 +69,4 @@ "function" ] } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index c645cee73c..7ef946bf05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,16 @@ "@angular-devkit/core" "8.0.0-beta.15" rxjs "6.4.0" +"@angular-devkit/build-optimizer@0.14.0-beta.5": + version "0.14.0-beta.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.14.0-beta.5.tgz#f842a0b2717517cdc8e40704076d6182feccb81a" + integrity sha512-sQ86BGrd65QD9fV+wgDWNFKS2kxsZFj/lSn3pjgguV43XjGvnNlXnsVAgZOruygyXjB/afEOkNpO/4sKFNxiMw== + dependencies: + loader-utils "1.2.3" + source-map "0.5.6" + typescript "3.2.4" + webpack-sources "1.3.0" + "@angular-devkit/build-optimizer@^0.800.0-beta.15": version "0.800.0-beta.15" resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.800.0-beta.15.tgz#1474df7321ec90e7a2ce95a8fdb65fe44c1bb201" @@ -64,29 +74,45 @@ universal-analytics "^0.4.20" uuid "^3.3.2" -"@bazel/bazel-darwin_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.24.0.tgz#828ef298d8d542961df388f17b0244f4f4302a74" - integrity sha512-xly44vkcD/fauUb7Lm5Lme4qhEZdkuuyBKSVQUHPbYAGDdbj/W8dupI3bZREkJAgG/WrRU+WXUemMj4U8ZcLcw== +"@babel/code-frame@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" -"@bazel/bazel-linux_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.24.0.tgz#9ef2e7266833ad2221fe4af4ceb6763d2897e3ff" - integrity sha512-p5ylPLWnJZDGbaIFBrtD/tp3Su5rMdzeeNJKU24XyiWQTHVZ3OD3I2Fb0ILCgfBjY8AlA7EtCtOI4hYnAuIOtg== +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" + integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" -"@bazel/bazel-win32_x64@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.24.0.tgz#02d83113a6c6ed99795a3e41bff5631aa141638d" - integrity sha512-/bcSEx+GoV/q7H4WM0jazfxTcurSiIIePhRv+d05mxRDcaWwhCO8KzmmZRWH1abW6npvq5tLkbSQi7G7nUBhgg== +"@bazel/bazel-darwin_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-darwin_x64/-/bazel-darwin_x64-0.26.0-rc5.tgz#237a564ee03abe16ced3622624e40fb5564a9d54" + integrity sha512-1zGZUtC2jzK0bwknMaXStMmfKul15sZ089EoaWPI596VyF3uNkcOMQWDDc5daDz3ZdxpvWYbpByInmXzWQIDzw== -"@bazel/bazel@0.24.0": - version "0.24.0" - resolved "https://registry.yarnpkg.com/@bazel/bazel/-/bazel-0.24.0.tgz#f4e68e3680ac299858c24c26be3d08d1151e78fc" - integrity sha512-/5E55tqH9ogAGF9Dd7RSCJmk7/xdlsPTAhsX3yEsEMs7GLdHlgD3jbeePsKUiHKKr8LXAufjTs2pXQfjrkZRMg== +"@bazel/bazel-linux_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-linux_x64/-/bazel-linux_x64-0.26.0-rc5.tgz#fc10602c13ca6ecb1afe76fb2f6c2773ba1204f2" + integrity sha512-HBNn4SL6zHBjA95dY/cpjnZEVB30+L2Uq4PWw48QhuQJGY/CYDwqWk2oFMUK47EEFMFh4OQfoN3q4oHKbvixdw== + +"@bazel/bazel-win32_x64@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel-win32_x64/-/bazel-win32_x64-0.26.0-rc5.tgz#05fecc69fdf36bc883e66452e01acff89356980e" + integrity sha512-DviEGJHXKlhpvnuqfT3hjyJmGhpsKykNxnZ60dDexlh0GkhIn9rN8OF3vj0EL+pRpYhiBQvcoiwQdvV2gBYbRw== + +"@bazel/bazel@0.26.0-rc5": + version "0.26.0-rc5" + resolved "https://registry.yarnpkg.com/@bazel/bazel/-/bazel-0.26.0-rc5.tgz#42b4ed498f02a426adee30583b98166611eb49e8" + integrity sha512-pZGUFXzvFUqm/hd7UmMPcCUc5CZd8dknae2pptQTpBhCFUHdsQeTr1alFKTNZPArVqAtqIUL2DS2MadKb3v31A== optionalDependencies: - "@bazel/bazel-darwin_x64" "0.24.0" - "@bazel/bazel-linux_x64" "0.24.0" - "@bazel/bazel-win32_x64" "0.24.0" + "@bazel/bazel-darwin_x64" "0.26.0-rc5" + "@bazel/bazel-linux_x64" "0.26.0-rc5" + "@bazel/bazel-win32_x64" "0.26.0-rc5" "@bazel/buildifier-darwin_x64@0.19.2": version "0.19.2" @@ -111,24 +137,25 @@ resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.9.0.tgz#fd60023acd36313d304cc2f8c2e181b88b5445cd" integrity sha512-E31cefDcdJsx/oii6p/gqKZXSVw0kEg1O73DD2McFcSvnf/p1GYWcQtVgdRQmlviBEytJkJgdX8rtThitRvcow== -"@bazel/jasmine@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-0.27.12.tgz#9319d1fb52723da8ec23d4ce507dc9eb202d4e31" - integrity sha512-olsKMLsfqA6F025EPn0glJal4DB0v4E2ISL+0Hu2piewAtjv8DARU0dxzyizgH1X20BooIlgq+ZL2cKdlXfMHg== +"@bazel/jasmine@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-0.29.0.tgz#347d9c512daf8576dcdaa9bb168733fa444c2263" + integrity sha512-QH/mLAH4e7gcJrfOT0BmJ4wk+5Ly3RU+RPLaCyacnCjmJCICukZJa/rrjbVtwd8u7ZM+Hf6tRaLLydSeKXGBug== dependencies: jasmine "~3.3.1" - v8-coverage "1.0.8" + jasmine-core "~3.3.0" + v8-coverage "1.0.9" -"@bazel/karma@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-0.27.12.tgz#a7c1648af93b4d376e4e3b6fc1c7006b4d79c092" - integrity sha512-LJAbNe8bR1vRLb8GqIbKzCpf8VMaTnM9jizxmsjkgdGjy9OgsseYbk6+70z1yx7s3QHPjj7JBqcx+d2yeXPo7g== +"@bazel/karma@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-0.29.0.tgz#bcb79750ecec3884136e584b36e3cd3b610455a5" + integrity sha512-dSN7iBAz0srGkJxAP7NAyWVdCWoUHBxlGL7YxHObV3kDTwWUDKWKcEwuOC7MTMSQtYM4WcVVvfCO3HhXKUxhPg== dependencies: jasmine-core "2.8.0" karma "^4.0.0" karma-chrome-launcher "2.2.0" karma-firefox-launcher "1.1.0" - karma-jasmine "1.1.1" + karma-jasmine "2.0.1" karma-requirejs "1.1.0" karma-sauce-launcher "2.0.2" karma-sourcemap-loader "0.3.7" @@ -136,10 +163,10 @@ semver "5.6.0" tmp "0.0.33" -"@bazel/typescript@0.27.12": - version "0.27.12" - resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.27.12.tgz#79a6468cd8da096c4da151fac658f7f801a59948" - integrity sha512-5cH+x7rvO8P9MCrd8YwJrTyzkET6MLajzngoV5yLDWwcokzs+b3yD9yoa5Vw3Dth2MdKRp+lyGMO7PwyAM3ebw== +"@bazel/typescript@0.29.0": + version "0.29.0" + resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-0.29.0.tgz#d276afe034f37b5f35ee1369c99dc33c637fc9f6" + integrity sha512-Dp5ucrE1vXTORGiwEi6Ur4dlICpLsmZ1dscsEQT4ywF7xTT0/NmIG0ecBghiCFPFQTxt1D05TR3SH06rPtTAew== dependencies: protobufjs "6.8.8" semver "5.6.0" @@ -527,11 +554,23 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897" integrity sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw== +"@types/node@^11.13.9": + version "11.13.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.13.10.tgz#4df59e5966b56f512bac98898bcbee5067411f0f" + integrity sha512-leUNzbFTMX94TWaIKz8N15Chu55F9QSH+INKayQr5xpkasBQBRF3qQXfo3/dOnMU/dEIit+Y/SU8HyOjq++GwA== + "@types/q@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= +"@types/resolve@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" + integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== + dependencies: + "@types/node" "*" + "@types/rx-core-binding@*": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz#d969d32f15a62b89e2862c17b3ee78fe329818d3" @@ -764,6 +803,11 @@ acorn@^6.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.5.tgz#81730c0815f3f3b34d8efa95cb7430965f4d887a" integrity sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg== +acorn@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -1746,6 +1790,11 @@ builtin-modules@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.0.0.tgz#1e587d44b006620d90286cc7a9238bbc6129cab1" integrity sha512-hMIeU4K2ilbXV6Uv93ZZ0Avg/M91RaKXucQ+4me2Do1txxBDyDZWCBa5bJSLqoNTRpXTLwEzIk1KmloenDDjhg== +builtin-modules@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" + integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -1926,7 +1975,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.1, chalk@^2.3.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1950,6 +1999,17 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +check-side-effects@0.0.20: + version "0.0.20" + resolved "https://registry.yarnpkg.com/check-side-effects/-/check-side-effects-0.0.20.tgz#b35cbb1f2eb210c1a051c7d78aa9fbd939ee31ea" + integrity sha512-tx7dvqAK0z8PM/gTTnfi6oW1Scn9f8EbMSZ1mu//4oIpNWF6ZNW2SFX7Cnsgq9xZjYeoD+YaUVx9CC0fEFizkQ== + dependencies: + "@angular-devkit/build-optimizer" "0.14.0-beta.5" + minimist "~1.2.0" + rollup "~1.11.3" + rollup-plugin-node-resolve "~4.2.3" + rollup-plugin-terser "~4.0.4" + chokidar@^1.0.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -2326,6 +2386,11 @@ commander@^2.13.0, commander@^2.7.1, commander@^2.8.1, commander@~2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +commander@^2.19.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + commander@^2.5.0, commander@^2.9.0: version "2.16.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" @@ -6034,6 +6099,11 @@ jasmine-core@^3.1.0: resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.1.0.tgz#a4785e135d5df65024dfc9224953df585bd2766c" integrity sha1-pHheE11d9lAk38kiSVPfWFvSdmw= +jasmine-core@^3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.4.0.tgz#2a74618e966026530c3518f03e9f845d26473ce3" + integrity sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg== + jasmine-core@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.3.0.tgz#dea1cdc634bc93c7e0d4ad27185df30fa971b10e" @@ -6061,6 +6131,14 @@ jasminewd2@^2.1.0: resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= +jest-worker@^24.0.0: + version "24.6.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.6.0.tgz#7f81ceae34b7cde0c9827a6980c35b7cdc0161b3" + integrity sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ== + dependencies: + merge-stream "^1.0.1" + supports-color "^6.1.0" + jetpack-id@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/jetpack-id/-/jetpack-id-0.0.4.tgz#6fc35a394a4aea190820a2ce7f23d2bb53512a9b" @@ -6151,6 +6229,11 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -6370,10 +6453,12 @@ karma-firefox-launcher@1.1.0: resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== -karma-jasmine@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.1.tgz#6fe840e75a11600c9d91e84b33c458e1c46a3529" - integrity sha1-b+hA51oRYAydkehLM8RY4cRqNSk= +karma-jasmine@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-2.0.1.tgz#26e3e31f2faf272dd80ebb0e1898914cc3a19763" + integrity sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA== + dependencies: + jasmine-core "^3.3" karma-jasmine@^1.1.2: version "1.1.2" @@ -7204,6 +7289,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + dependencies: + readable-stream "^2.0.1" + methmeth@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/methmeth/-/methmeth-1.1.0.tgz#e80a26618e52f5c4222861bb748510bd10e29089" @@ -7373,7 +7465,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= @@ -7726,7 +7818,7 @@ node-source-walk@~1.4.0: dependencies: acorn "^1.0.3" -node-uuid@1.4.8, node-uuid@^1.4.8, node-uuid@~1.4.7: +node-uuid@1.4.8, node-uuid@~1.4.7: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= @@ -8888,6 +8980,14 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + read-pkg@^1.0.0, read-pkg@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -9389,6 +9489,13 @@ rollup-plugin-commonjs@^9.2.1: resolve "^1.10.0" rollup-pluginutils "^2.3.3" +rollup-plugin-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz#a18da0a4b30bf5ca1ee76ddb1422afbb84ae2b9e" + integrity sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow== + dependencies: + rollup-pluginutils "^2.5.0" + rollup-plugin-node-resolve@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.0.1.tgz#f95765d174e5daeef9ea6268566141f53aa9d422" @@ -9398,6 +9505,16 @@ rollup-plugin-node-resolve@^4.0.0: is-module "^1.0.0" resolve "^1.10.0" +rollup-plugin-node-resolve@~4.2.3: + version "4.2.4" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.2.4.tgz#7d370f8d6fd3031006a0032c38262dd9be3c6250" + integrity sha512-t/64I6l7fZ9BxqD3XlX4ZeO6+5RLKyfpwE2CiPNUKa+GocPlQhf/C208ou8y3AwtNsc6bjSk/8/6y/YAyxCIvw== + dependencies: + "@types/resolve" "0.0.8" + builtin-modules "^3.1.0" + is-module "^1.0.0" + resolve "^1.10.0" + rollup-plugin-sourcemaps@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.4.2.tgz#62125aa94087aadf7b83ef4dfaf629b473135e87" @@ -9406,6 +9523,16 @@ rollup-plugin-sourcemaps@^0.4.2: rollup-pluginutils "^2.0.1" source-map-resolve "^0.5.0" +rollup-plugin-terser@~4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz#6f661ef284fa7c27963d242601691dc3d23f994e" + integrity sha512-wPANT5XKVJJ8RDUN0+wIr7UPd0lIXBo4UdJ59VmlPCtlFsE20AM+14pe+tk7YunCsWEiuzkDBY3QIkSCjtrPXg== + dependencies: + "@babel/code-frame" "^7.0.0" + jest-worker "^24.0.0" + serialize-javascript "^1.6.1" + terser "^3.14.1" + rollup-pluginutils@^2.0.1: version "2.3.0" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.0.tgz#478ace04bd7f6da2e724356ca798214884738fc4" @@ -9422,6 +9549,14 @@ rollup-pluginutils@^2.3.3: estree-walker "^0.6.0" micromatch "^3.1.10" +rollup-pluginutils@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.6.0.tgz#203706edd43dfafeaebc355d7351119402fc83ad" + integrity sha512-aGQwspEF8oPKvg37u3p7h0cYNwmJR1sCBMZGZ5b9qy8HGtETknqjzcxrDRrcAnJNXN18lBH4Q9vZYth/p4n8jQ== + dependencies: + estree-walker "^0.6.0" + micromatch "^3.1.10" + rollup@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.1.0.tgz#461a7534b55be48aa4a6e6810a1543a5769e75d1" @@ -9431,6 +9566,15 @@ rollup@^1.1.0: "@types/node" "*" acorn "^6.0.5" +rollup@~1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.11.3.tgz#6f436db2a2d6b63f808bf60ad01a177643dedb81" + integrity sha512-81MR7alHcFKxgWzGfG7jSdv+JQxSOIOD/Fa3iNUmpzbd7p+V19e1l9uffqT8/7YAHgGOzmoPGN3Fx3L2ptOf5g== + dependencies: + "@types/estree" "0.0.39" + "@types/node" "^11.13.9" + acorn "^6.1.1" + router@^1.3.1: version "1.3.3" resolved "https://registry.yarnpkg.com/router/-/router-1.3.3.tgz#c142f6b5ea4d6b3359022ca95b6580bd217f89cf" @@ -9655,6 +9799,11 @@ sequencify@~0.0.7: resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" integrity sha1-kM/xnQLgcCf9dn9erT57ldHnOAw= +serialize-javascript@^1.6.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" + integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== + serializerr@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91" @@ -9972,6 +10121,14 @@ source-map-support@~0.4.0: dependencies: source-map "^0.5.6" +source-map-support@~0.5.10: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -10472,6 +10629,13 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + symbol-observable@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -10528,16 +10692,24 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -test-exclude@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" - integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== +terser@^3.14.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" + commander "^2.19.0" + source-map "~0.6.1" + source-map-support "~0.5.10" + +test-exclude@^5.2.2: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" text-extensions@^1.0.0: version "1.7.0" @@ -10805,6 +10977,11 @@ tslint-eslint-rules@4.1.1: tslib "^1.0.0" tsutils "^1.4.0" +tslint-no-toplevel-property-access@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/tslint-no-toplevel-property-access/-/tslint-no-toplevel-property-access-0.0.2.tgz#c9b19bbd525ea7b8577e5ada601cc8625b4ed004" + integrity sha512-Oc+UUurlGLBkgeUSGxMoTpRUpaXsjqzQCEAYrYQyuU8330fi5FKlye5n53y87EJ24AlfdoxMPV7DJfFOADapfg== + tslint@5.7.0: version "5.7.0" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552" @@ -10900,6 +11077,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d" + integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg== + typescript@3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3.tgz#0eb320e4ace9b10eadf5bc6103286b0f8b7c224f" @@ -11233,10 +11415,10 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -v8-coverage@1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/v8-coverage/-/v8-coverage-1.0.8.tgz#3393cb904cd064e2e56e747781641c75a6e7f52c" - integrity sha512-DWNS16h1LKyRMZsJ7+2KFUhA4hGbWUWTcUbDwnT6WMQKbolixY1KCSUaw2NVqtpwODGtqCWvHUjQjwfh562U0A== +v8-coverage@1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/v8-coverage/-/v8-coverage-1.0.9.tgz#780889680c0fea0f587adf22e2b5f443b9434745" + integrity sha512-JolsCH1JDI2QULrxkAGZaovJPvg/Q0p20Uj0F5N8fPtYDtz38gNBRPQ/WVXlLLd3d8WHvKN96AfE4XFk4u0g2g== dependencies: debug "^3.1.0" foreground-child "^1.5.6" @@ -11244,11 +11426,11 @@ v8-coverage@1.0.8: istanbul-lib-report "^1.1.3" istanbul-reports "^1.3.0" mkdirp "^0.5.1" - node-uuid "^1.4.8" rimraf "^2.6.2" signal-exit "^3.0.2" spawn-wrap "^1.4.2" - test-exclude "^4.2.1" + test-exclude "^5.2.2" + uuid "^3.3.2" v8-to-istanbul "1.2.0" yargs "^11.0.0"