mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 10:08:57 +00:00
Compare commits
1 Commits
httpwithst
...
rel/2.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
287c10fd2e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,5 +40,3 @@ npm-debug.log
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
/templates/*/Properties/launchSettings.json
|
/templates/*/Properties/launchSettings.json
|
||||||
global.json
|
|
||||||
korebuild-lock.txt
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
|
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-release/api/v3/index.json" />
|
||||||
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
||||||
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
init:
|
init:
|
||||||
- git config --global core.autocrlf true
|
- git config --global core.autocrlf true
|
||||||
install:
|
install:
|
||||||
- ps: Install-Product node 6.9.2 x64
|
- ps: Install-Product node 6.9.2 x64
|
||||||
@@ -25,10 +25,6 @@ artifacts:
|
|||||||
type: NuGetPackage
|
type: NuGetPackage
|
||||||
# - ps: .\build.ps1
|
# - ps: .\build.ps1
|
||||||
clone_depth: 1
|
clone_depth: 1
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
|
||||||
test_script:
|
test_script:
|
||||||
- dotnet restore
|
- dotnet restore
|
||||||
- ps: Push-Location
|
- ps: Push-Location
|
||||||
|
|||||||
226
build.ps1
226
build.ps1
@@ -1,177 +1,67 @@
|
|||||||
#!/usr/bin/env powershell
|
$ErrorActionPreference = "Stop"
|
||||||
#requires -version 4
|
|
||||||
|
|
||||||
<#
|
function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries)
|
||||||
.SYNOPSIS
|
{
|
||||||
Build this repository
|
while($true)
|
||||||
|
{
|
||||||
.DESCRIPTION
|
try
|
||||||
Downloads korebuild if required. Then builds the repository.
|
{
|
||||||
|
Invoke-WebRequest $url -OutFile $downloadLocation
|
||||||
.PARAMETER Path
|
break
|
||||||
The folder to build. Defaults to the folder containing this script.
|
|
||||||
|
|
||||||
.PARAMETER Channel
|
|
||||||
The channel of KoreBuild to download. Overrides the value from the config file.
|
|
||||||
|
|
||||||
.PARAMETER DotNetHome
|
|
||||||
The directory where .NET Core tools will be stored.
|
|
||||||
|
|
||||||
.PARAMETER ToolsSource
|
|
||||||
The base url where build tools can be downloaded. Overrides the value from the config file.
|
|
||||||
|
|
||||||
.PARAMETER Update
|
|
||||||
Updates KoreBuild to the latest version even if a lock file is present.
|
|
||||||
|
|
||||||
.PARAMETER ConfigFile
|
|
||||||
The path to the configuration file that stores values. Defaults to version.xml.
|
|
||||||
|
|
||||||
.PARAMETER MSBuildArgs
|
|
||||||
Arguments to be passed to MSBuild
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be.
|
|
||||||
When the lockfile is not present, KoreBuild will create one using latest available version from $Channel.
|
|
||||||
|
|
||||||
The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well.
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
Example config file:
|
|
||||||
```xml
|
|
||||||
<!-- version.xml -->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<KoreBuildChannel>dev</KoreBuildChannel>
|
|
||||||
<KoreBuildToolsSource>https://aspnetcore.blob.core.windows.net/buildtools</KoreBuildToolsSource>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
#>
|
|
||||||
[CmdletBinding(PositionalBinding = $false)]
|
|
||||||
param(
|
|
||||||
[string]$Path = $PSScriptRoot,
|
|
||||||
[Alias('c')]
|
|
||||||
[string]$Channel,
|
|
||||||
[Alias('d')]
|
|
||||||
[string]$DotNetHome,
|
|
||||||
[Alias('s')]
|
|
||||||
[string]$ToolsSource,
|
|
||||||
[Alias('u')]
|
|
||||||
[switch]$Update,
|
|
||||||
[string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'),
|
|
||||||
[Parameter(ValueFromRemainingArguments = $true)]
|
|
||||||
[string[]]$MSBuildArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version 2
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
#
|
|
||||||
# Functions
|
|
||||||
#
|
|
||||||
|
|
||||||
function Get-KoreBuild {
|
|
||||||
|
|
||||||
$lockFile = Join-Path $Path 'korebuild-lock.txt'
|
|
||||||
|
|
||||||
if (!(Test-Path $lockFile) -or $Update) {
|
|
||||||
Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile
|
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
$exceptionMessage = $_.Exception.Message
|
||||||
|
Write-Host "Failed to download '$url': $exceptionMessage"
|
||||||
|
if ($retries -gt 0) {
|
||||||
|
$retries--
|
||||||
|
Write-Host "Waiting 10 seconds before retrying. Retries left: $retries"
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
|
||||||
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
|
||||||
if (!$version) {
|
|
||||||
Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'"
|
|
||||||
}
|
}
|
||||||
$version = $version.TrimStart('version:').Trim()
|
else
|
||||||
$korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version)
|
{
|
||||||
|
$exception = $_.Exception
|
||||||
if (!(Test-Path $korebuildPath)) {
|
throw $exception
|
||||||
Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version"
|
|
||||||
New-Item -ItemType Directory -Path $korebuildPath | Out-Null
|
|
||||||
$remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip"
|
|
||||||
|
|
||||||
try {
|
|
||||||
$tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip"
|
|
||||||
Get-RemoteFile $remotePath $tmpfile
|
|
||||||
if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) {
|
|
||||||
# Use built-in commands where possible as they are cross-plat compatible
|
|
||||||
Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath
|
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
# Fallback to old approach for old installations of PowerShell
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cd $PSScriptRoot
|
||||||
|
|
||||||
|
$repoFolder = $PSScriptRoot
|
||||||
|
$env:REPO_FOLDER = $repoFolder
|
||||||
|
|
||||||
|
$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip"
|
||||||
|
if ($env:KOREBUILD_ZIP)
|
||||||
|
{
|
||||||
|
$koreBuildZip=$env:KOREBUILD_ZIP
|
||||||
|
}
|
||||||
|
|
||||||
|
$buildFolder = ".build"
|
||||||
|
$buildFile="$buildFolder\KoreBuild.ps1"
|
||||||
|
|
||||||
|
if (!(Test-Path $buildFolder)) {
|
||||||
|
Write-Host "Downloading KoreBuild from $koreBuildZip"
|
||||||
|
|
||||||
|
$tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid()
|
||||||
|
New-Item -Path "$tempFolder" -Type directory | Out-Null
|
||||||
|
|
||||||
|
$localZipFile="$tempFolder\korebuild.zip"
|
||||||
|
|
||||||
|
DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6
|
||||||
|
|
||||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath)
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder)
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore
|
|
||||||
throw
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Remove-Item $tmpfile -ErrorAction Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $korebuildPath
|
New-Item -Path "$buildFolder" -Type directory | Out-Null
|
||||||
|
copy-item "$tempFolder\**\build\*" $buildFolder -Recurse
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
if (Test-Path $tempFolder) {
|
||||||
|
Remove-Item -Recurse -Force $tempFolder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Join-Paths([string]$path, [string[]]$childPaths) {
|
&"$buildFile" @args
|
||||||
$childPaths | ForEach-Object { $path = Join-Path $path $_ }
|
|
||||||
return $path
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
|
|
||||||
if ($RemotePath -notlike 'http*') {
|
|
||||||
Copy-Item $RemotePath $LocalPath
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$retries = 10
|
|
||||||
while ($retries -gt 0) {
|
|
||||||
$retries -= 1
|
|
||||||
try {
|
|
||||||
Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath
|
|
||||||
return
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Verbose "Request failed. $retries retries remaining"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Error "Download failed: '$RemotePath'."
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Main
|
|
||||||
#
|
|
||||||
|
|
||||||
# Load configuration or set defaults
|
|
||||||
|
|
||||||
if (Test-Path $ConfigFile) {
|
|
||||||
[xml] $config = Get-Content $ConfigFile
|
|
||||||
if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' }
|
|
||||||
if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$DotNetHome) {
|
|
||||||
$DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } `
|
|
||||||
elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} `
|
|
||||||
elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}`
|
|
||||||
else { Join-Path $PSScriptRoot '.dotnet'}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$Channel) { $Channel = 'dev' }
|
|
||||||
if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' }
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
|
|
||||||
$korebuildPath = Get-KoreBuild
|
|
||||||
Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1')
|
|
||||||
|
|
||||||
try {
|
|
||||||
Install-Tools $ToolsSource $DotNetHome
|
|
||||||
Invoke-RepositoryBuild $Path @MSBuildArgs
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Remove-Module 'KoreBuild' -ErrorAction Ignore
|
|
||||||
}
|
|
||||||
|
|||||||
223
build.sh
223
build.sh
@@ -1,197 +1,46 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd $repoFolder
|
||||||
|
|
||||||
set -euo pipefail
|
koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip"
|
||||||
|
if [ ! -z $KOREBUILD_ZIP ]; then
|
||||||
|
koreBuildZip=$KOREBUILD_ZIP
|
||||||
|
fi
|
||||||
|
|
||||||
#
|
buildFolder=".build"
|
||||||
# variables
|
buildFile="$buildFolder/KoreBuild.sh"
|
||||||
#
|
|
||||||
|
|
||||||
RESET="\033[0m"
|
if test ! -d $buildFolder; then
|
||||||
RED="\033[0;31m"
|
echo "Downloading KoreBuild from $koreBuildZip"
|
||||||
MAGENTA="\033[0;95m"
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet"
|
|
||||||
config_file="$DIR/version.xml"
|
|
||||||
verbose=false
|
|
||||||
update=false
|
|
||||||
repo_path="$DIR"
|
|
||||||
channel=''
|
|
||||||
tools_source=''
|
|
||||||
|
|
||||||
#
|
tempFolder="/tmp/KoreBuild-$(uuidgen)"
|
||||||
# Functions
|
mkdir $tempFolder
|
||||||
#
|
|
||||||
__usage() {
|
|
||||||
echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] [[--] <MSBUILD_ARG>...]"
|
|
||||||
echo ""
|
|
||||||
echo "Arguments:"
|
|
||||||
echo " <MSBUILD_ARG>... Arguments passed to MSBuild. Variable number of arguments allowed."
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --verbose Show verbose output."
|
|
||||||
echo " -c|--channel <CHANNEL> The channel of KoreBuild to download. Overrides the value from the config file.."
|
|
||||||
echo " --config-file <FILE> TThe path to the configuration file that stores values. Defaults to version.xml."
|
|
||||||
echo " -d|--dotnet-home <DIR> The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
|
|
||||||
echo " --path <PATH> The directory to build. Defaults to the directory containing the script."
|
|
||||||
echo " -s|--tools-source <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
|
|
||||||
echo " -u|--update Update to the latest KoreBuild even if the lock file is present."
|
|
||||||
echo ""
|
|
||||||
echo "Description:"
|
|
||||||
echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be."
|
|
||||||
echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel."
|
|
||||||
|
|
||||||
if [[ "${1:-}" != '--no-exit' ]]; then
|
localZipFile="$tempFolder/korebuild.zip"
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
get_korebuild() {
|
retries=6
|
||||||
local version
|
until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null)
|
||||||
local lock_file="$repo_path/korebuild-lock.txt"
|
do
|
||||||
if [ ! -f "$lock_file" ] || [ "$update" = true ]; then
|
echo "Failed to download '$koreBuildZip'"
|
||||||
__get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file"
|
if [ "$retries" -le 0 ]; then
|
||||||
fi
|
|
||||||
version="$(grep 'version:*' -m 1 "$lock_file")"
|
|
||||||
if [[ "$version" == '' ]]; then
|
|
||||||
__error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
||||||
local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version"
|
|
||||||
|
|
||||||
{
|
|
||||||
if [ ! -d "$korebuild_path" ]; then
|
|
||||||
mkdir -p "$korebuild_path"
|
|
||||||
local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip"
|
|
||||||
tmpfile="$(mktemp)"
|
|
||||||
echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}"
|
|
||||||
if __get_remote_file "$remote_path" "$tmpfile"; then
|
|
||||||
unzip -q -d "$korebuild_path" "$tmpfile"
|
|
||||||
fi
|
|
||||||
rm "$tmpfile" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$korebuild_path/KoreBuild.sh"
|
|
||||||
} || {
|
|
||||||
if [ -d "$korebuild_path" ]; then
|
|
||||||
echo "Cleaning up after failed installation"
|
|
||||||
rm -rf "$korebuild_path" || true
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
__error() {
|
|
||||||
echo -e "${RED}$*${RESET}" 1>&2
|
|
||||||
}
|
|
||||||
|
|
||||||
__machine_has() {
|
|
||||||
hash "$1" > /dev/null 2>&1
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
__get_remote_file() {
|
|
||||||
local remote_path=$1
|
|
||||||
local local_path=$2
|
|
||||||
|
|
||||||
if [[ "$remote_path" != 'http'* ]]; then
|
|
||||||
cp "$remote_path" "$local_path"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
failed=false
|
|
||||||
if __machine_has wget; then
|
|
||||||
wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$failed" = true ] && __machine_has curl; then
|
|
||||||
failed=false
|
|
||||||
curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$failed" = true ]; then
|
|
||||||
__error "Download failed: $remote_path" 1>&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
__read_dom () { local IFS=\> ; read -r -d \< ENTITY CONTENT ;}
|
|
||||||
|
|
||||||
#
|
|
||||||
# main
|
|
||||||
#
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
-\?|-h|--help)
|
|
||||||
__usage --no-exit
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
-c|--channel|-Channel)
|
|
||||||
shift
|
|
||||||
channel="${1:-}"
|
|
||||||
[ -z "$channel" ] && __usage
|
|
||||||
;;
|
|
||||||
--config-file|-ConfigFile)
|
|
||||||
shift
|
|
||||||
config_file="${1:-}"
|
|
||||||
[ -z "$config_file" ] && __usage
|
|
||||||
;;
|
|
||||||
-d|--dotnet-home|-DotNetHome)
|
|
||||||
shift
|
|
||||||
DOTNET_HOME="${1:-}"
|
|
||||||
[ -z "$DOTNET_HOME" ] && __usage
|
|
||||||
;;
|
|
||||||
--path|-Path)
|
|
||||||
shift
|
|
||||||
repo_path="${1:-}"
|
|
||||||
[ -z "$repo_path" ] && __usage
|
|
||||||
;;
|
|
||||||
-s|--tools-source|-ToolsSource)
|
|
||||||
shift
|
|
||||||
tools_source="${1:-}"
|
|
||||||
[ -z "$tools_source" ] && __usage
|
|
||||||
;;
|
|
||||||
-u|--update|-Update)
|
|
||||||
update=true
|
|
||||||
;;
|
|
||||||
--verbose|-Verbose)
|
|
||||||
verbose=true
|
|
||||||
;;
|
|
||||||
--)
|
|
||||||
shift
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
if ! __machine_has unzip; then
|
|
||||||
__error 'Missing required command: unzip'
|
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
|
retries=$((retries - 1))
|
||||||
|
echo "Waiting 10 seconds before retrying. Retries left: $retries"
|
||||||
|
sleep 10s
|
||||||
|
done
|
||||||
|
|
||||||
|
unzip -q -d $tempFolder $localZipFile
|
||||||
|
|
||||||
|
mkdir $buildFolder
|
||||||
|
cp -r $tempFolder/**/build/** $buildFolder
|
||||||
|
|
||||||
|
chmod +x $buildFile
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
if test -d $tempFolder; then
|
||||||
|
rm -rf $tempFolder
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! __machine_has curl && ! __machine_has wget; then
|
$buildFile -r $repoFolder "$@"
|
||||||
__error 'Missing required command. Either wget or curl is required.'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$config_file" ]; then
|
|
||||||
comment=false
|
|
||||||
while __read_dom; do
|
|
||||||
if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi
|
|
||||||
if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi
|
|
||||||
if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi
|
|
||||||
if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi
|
|
||||||
done < "$config_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -z "$channel" ] && channel='dev'
|
|
||||||
[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
|
|
||||||
|
|
||||||
get_korebuild
|
|
||||||
install_tools "$tools_source" "$DOTNET_HOME"
|
|
||||||
invoke_repository_build "$repo_path" "$@"
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<Import Project="dependencies.props" />
|
<Import Project="dependencies.props" />
|
||||||
<Import Project="..\version.xml" />
|
<Import Project="..\version.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Product>Microsoft ASP.NET Core</Product>
|
<Product>Microsoft ASP.NET Core</Product>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AspNetCoreVersion>2.1.0-*</AspNetCoreVersion>
|
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
|
||||||
<InternalAspNetCoreSdkVersion>2.1.1-*</InternalAspNetCoreSdkVersion>
|
<AutoMapperVersion>5.0.2</AutoMapperVersion>
|
||||||
|
<InternalAspNetCoreSdkVersion>2.0.1-*</InternalAspNetCoreSdkVersion>
|
||||||
<JsonNetVersion>10.0.1</JsonNetVersion>
|
<JsonNetVersion>10.0.1</JsonNetVersion>
|
||||||
<NETStandardImplicitPackageVersion>2.0.0-*</NETStandardImplicitPackageVersion>
|
<NETStandardImplicitPackageVersion>2.0.0-*</NETStandardImplicitPackageVersion>
|
||||||
<NETStandardLibraryNETFrameworkVersion>2.0.0-*</NETStandardLibraryNETFrameworkVersion>
|
<NETStandardLibraryNETFrameworkVersion>2.0.0-*</NETStandardLibraryNETFrameworkVersion>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
options.DebuggingPort)
|
options.DebuggingPort)
|
||||||
{
|
{
|
||||||
_client = new HttpClient();
|
_client = new HttpClient();
|
||||||
_client.Timeout = TimeSpan.FromMilliseconds(options.InvocationTimeoutMilliseconds + 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MakeCommandLineOptions(int port)
|
private static string MakeCommandLineOptions(int port)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using System.Threading;
|
|||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
using Microsoft.AspNetCore.SpaServices.Webpack;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
{
|
{
|
||||||
@@ -88,7 +87,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
};
|
};
|
||||||
var devServerInfo =
|
var devServerInfo =
|
||||||
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
||||||
JsonConvert.SerializeObject(devServerOptions, new JsonSerializerSettings() { ContractResolver = new DefaultContractResolver() })).Result;
|
JsonConvert.SerializeObject(devServerOptions)).Result;
|
||||||
|
|
||||||
// If we're talking to an older version of aspnet-webpack, it will return only a single PublicPath,
|
// If we're talking to an older version of aspnet-webpack, it will return only a single PublicPath,
|
||||||
// not an array of PublicPaths. Handle that scenario.
|
// not an array of PublicPaths. Handle that scenario.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
/node_modules/
|
|
||||||
**/*.js
|
|
||||||
**/*.d.ts
|
|
||||||
**/*.metadata.json
|
|
||||||
/compiled
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
!/*.js
|
|
||||||
!/*.d.ts
|
|
||||||
/compiled
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
||||||
these files 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.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "aspnet-angular",
|
|
||||||
"version": "0.1.1",
|
|
||||||
"description": "Helpers for using Angular in ASP.NET Core projects",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"prepublish": "rimraf *.d.ts && ngc && echo 'Finished building NPM package \"aspnet-angular\"'",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/aspnet/JavaScriptServices.git"
|
|
||||||
},
|
|
||||||
"author": "Microsoft",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/aspnet/JavaScriptServices/issues"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@angular/common": "^4.3.2",
|
|
||||||
"@angular/compiler": "^4.3.2",
|
|
||||||
"@angular/compiler-cli": "^4.3.2",
|
|
||||||
"@angular/core": "^4.3.2",
|
|
||||||
"@angular/http": "^4.3.2",
|
|
||||||
"@angular/platform-browser": "^4.3.2",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"rxjs": "^5.4.2",
|
|
||||||
"zone.js": "^0.8.16"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/core": "^4.2.5 || ^5.0.0-beta"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Provider, NgModule, Inject } from '@angular/core';
|
|
||||||
import { Headers, Http, ResponseOptions, RequestOptionsArgs, Response } from '@angular/http';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import 'rxjs/add/observable/of';
|
|
||||||
import 'rxjs/add/operator/map';
|
|
||||||
const globalSerializedStateKey = 'HTTP_STATE_TRANSFER';
|
|
||||||
const backingStoreDIToken = 'HTTP_STATE_BACKING_STORE';
|
|
||||||
|
|
||||||
export interface CacheOptions {
|
|
||||||
permanent: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CachedHttpResponse {
|
|
||||||
headers: { [name: string]: any } | null;
|
|
||||||
status: number;
|
|
||||||
statusText: string | null;
|
|
||||||
text: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BackingStore = { [key: string]: CachedHttpResponse };
|
|
||||||
|
|
||||||
export class HttpWithStateTransfer {
|
|
||||||
private backingStore: BackingStore;
|
|
||||||
private http: Http;
|
|
||||||
|
|
||||||
constructor(@Inject(Http) http: Http, @Inject(backingStoreDIToken) backingStore: BackingStore) {
|
|
||||||
this.http = http;
|
|
||||||
this.backingStore = backingStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public stateForTransfer(): any {
|
|
||||||
return { [globalSerializedStateKey]: this.backingStore };
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(url: string, options?: CacheOptions, requestOptions?: RequestOptionsArgs): Observable<Response> {
|
|
||||||
return this.getCachedResponse(/* cacheKey */ url, () => this.http.get(url, requestOptions), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCachedResponse(cacheKey: string, provider: () => Observable<Response>, options?: CacheOptions): Observable<Response> {
|
|
||||||
// By default, the cache is only used for the *first* client-side read. So, we're only performing
|
|
||||||
// a one-time transfer of server-side response to the client. If you want to keep and reuse cached
|
|
||||||
// responses continually during server-side and client-side execution, set 'permanent' to 'true.
|
|
||||||
const isClient = typeof window !== 'undefined';
|
|
||||||
const isPermanent = options && options.permanent;
|
|
||||||
|
|
||||||
const allowReadFromCache = isClient || isPermanent;
|
|
||||||
if (allowReadFromCache && this.backingStore.hasOwnProperty(cacheKey)) {
|
|
||||||
const cachedValue = this.backingStore[cacheKey];
|
|
||||||
if (!isPermanent) {
|
|
||||||
delete this.backingStore[cacheKey];
|
|
||||||
}
|
|
||||||
return Observable.of(new Response(new ResponseOptions({
|
|
||||||
body: cachedValue.text,
|
|
||||||
headers: new Headers(cachedValue.headers),
|
|
||||||
status: cachedValue.status,
|
|
||||||
url: cachedValue.url
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider()
|
|
||||||
.map(response => {
|
|
||||||
const allowWriteToCache = !isClient || isPermanent;
|
|
||||||
if (allowWriteToCache) {
|
|
||||||
this.backingStore[cacheKey] = {
|
|
||||||
headers: response.headers ? response.headers.toJSON() : null,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
text: response.text(),
|
|
||||||
url: response.url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultBackingStoreFactory() {
|
|
||||||
const transferredData = typeof window !== 'undefined' ? (window as any)[globalSerializedStateKey] : null;
|
|
||||||
return transferredData || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
providers: [
|
|
||||||
// The backing store is a separate DI service so you could override exactly how it gets
|
|
||||||
// transferred from server to client
|
|
||||||
{ provide: backingStoreDIToken, useFactory: defaultBackingStoreFactory },
|
|
||||||
|
|
||||||
{ provide: HttpWithStateTransfer, useClass: HttpWithStateTransfer },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class HttpWithStateTransferModule {
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './HttpWithStateTransfer';
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": ".",
|
|
||||||
"lib": ["es2015", "dom"]
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"angularCompilerOptions": {
|
|
||||||
"genDir": "compiled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { HttpWithStateTransferModule } from 'aspnet-angular';
|
|
||||||
|
|
||||||
import { AppComponent } from './components/app/app.component';
|
import { AppComponent } from './components/app/app.component';
|
||||||
import { NavMenuComponent } from './components/navmenu/navmenu.component';
|
import { NavMenuComponent } from './components/navmenu/navmenu.component';
|
||||||
@@ -22,7 +21,6 @@ import { CounterComponent } from './components/counter/counter.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
HttpWithStateTransferModule,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
import { HttpWithStateTransfer } from 'aspnet-angular';
|
import { Http } from '@angular/http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fetchdata',
|
selector: 'fetchdata',
|
||||||
@@ -8,7 +8,7 @@ import { HttpWithStateTransfer } from 'aspnet-angular';
|
|||||||
export class FetchDataComponent {
|
export class FetchDataComponent {
|
||||||
public forecasts: WeatherForecast[];
|
public forecasts: WeatherForecast[];
|
||||||
|
|
||||||
constructor(http: HttpWithStateTransfer, @Inject('BASE_URL') baseUrl: string) {
|
constructor(http: Http, @Inject('BASE_URL') baseUrl: string) {
|
||||||
http.get(baseUrl + 'api/SampleData/WeatherForecasts').subscribe(result => {
|
http.get(baseUrl + 'api/SampleData/WeatherForecasts').subscribe(result => {
|
||||||
this.forecasts = result.json() as WeatherForecast[];
|
this.forecasts = result.json() as WeatherForecast[];
|
||||||
}, error => console.error(error));
|
}, error => console.error(error));
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
|
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
|
||||||
<li><strong>Server-side prerendering</strong>. For faster initial loading and improved SEO, your Angular app is prerendered on the server. The resulting HTML is then transferred to the browser where a client-side copy of the app takes over.</li>
|
<li><strong>Server-side prerendering</strong>. For faster initial loading and improved SEO, your Angular app is prerendered on the server. The resulting HTML is then transferred to the browser where a client-side copy of the app takes over.</li>
|
||||||
<li><strong>Webpack dev middleware</strong>. In development mode, there's no need to run the <code>webpack</code> build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.</li>
|
<li><strong>Webpack dev middleware</strong>. In development mode, there's no need to run the <code>webpack</code> build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.</li>
|
||||||
<li><strong>Hot module replacement</strong>. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Angular app will be rebuilt and a new instance injected into the page.</li>
|
<li><strong>Hot module replacement</strong>. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Angular app will be rebuilt and a new instance injected is into the page.</li>
|
||||||
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and the <code>webpack</code> build tool produces minified static CSS and JavaScript files.</li>
|
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and the <code>webpack</code> build tool produces minified static CSS and JavaScript files.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/
|
|||||||
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
|
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
|
||||||
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
|
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
|
||||||
import { AppModule } from './app/app.module.server';
|
import { AppModule } from './app/app.module.server';
|
||||||
import { HttpWithStateTransfer } from 'aspnet-angular';
|
|
||||||
|
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ export default createServerRenderer(params => {
|
|||||||
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
|
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
|
||||||
const state = moduleRef.injector.get(PlatformState);
|
const state = moduleRef.injector.get(PlatformState);
|
||||||
const zone = moduleRef.injector.get(NgZone);
|
const zone = moduleRef.injector.get(NgZone);
|
||||||
const http: HttpWithStateTransfer = moduleRef.injector.get(HttpWithStateTransfer);
|
|
||||||
|
|
||||||
return new Promise<RenderResult>((resolve, reject) => {
|
return new Promise<RenderResult>((resolve, reject) => {
|
||||||
zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
|
zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
|
||||||
@@ -30,8 +28,7 @@ export default createServerRenderer(params => {
|
|||||||
// completing the request in case there's an error to report
|
// completing the request in case there's an error to report
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
resolve({
|
resolve({
|
||||||
html: state.renderToString(),
|
html: state.renderToString()
|
||||||
globals: { ...http.stateForTransfer() }
|
|
||||||
});
|
});
|
||||||
moduleRef.destroy();
|
moduleRef.destroy();
|
||||||
});
|
});
|
||||||
|
|||||||
5
templates/AngularSpa/npm-shrinkwrap.json
generated
5
templates/AngularSpa/npm-shrinkwrap.json
generated
@@ -273,11 +273,6 @@
|
|||||||
"from": "asn1.js@>=4.0.0 <5.0.0",
|
"from": "asn1.js@>=4.0.0 <5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz"
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz"
|
||||||
},
|
},
|
||||||
"aspnet-angular": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"from": "aspnet-angular@0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/aspnet-angular/-/aspnet-angular-0.1.1.tgz"
|
|
||||||
},
|
|
||||||
"aspnet-prerendering": {
|
"aspnet-prerendering": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"from": "aspnet-prerendering@3.0.1",
|
"from": "aspnet-prerendering@3.0.1",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@ngtools/webpack": "1.5.0",
|
"@ngtools/webpack": "1.5.0",
|
||||||
"@types/webpack-env": "1.13.0",
|
"@types/webpack-env": "1.13.0",
|
||||||
"angular2-template-loader": "0.6.2",
|
"angular2-template-loader": "0.6.2",
|
||||||
"aspnet-angular": "^0.1.1",
|
|
||||||
"aspnet-prerendering": "^3.0.1",
|
"aspnet-prerendering": "^3.0.1",
|
||||||
"aspnet-webpack": "^2.0.1",
|
"aspnet-webpack": "^2.0.1",
|
||||||
"awesome-typescript-loader": "3.2.1",
|
"awesome-typescript-loader": "3.2.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "generator-aspnetcore-spa",
|
"name": "generator-aspnetcore-spa",
|
||||||
"version": "0.9.4",
|
"version": "1.0.0",
|
||||||
"description": "Single-Page App templates for ASP.NET Core",
|
"description": "Single-Page App templates for ASP.NET Core",
|
||||||
"author": "Microsoft",
|
"author": "Microsoft",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
7
version.props
Normal file
7
version.props
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- This file may be overwritten by automation. Only values allowed here are VersionPrefix and VersionSuffix. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionPrefix>2.0.0</VersionPrefix>
|
||||||
|
<VersionSuffix>rtm</VersionSuffix>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<!-- This file may be overwritten by automation. -->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<KoreBuildChannel>dev</KoreBuildChannel>
|
|
||||||
<VersionPrefix>2.1.0</VersionPrefix>
|
|
||||||
<VersionSuffix>preview1</VersionSuffix>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
Reference in New Issue
Block a user