Initial commit: Aeroflot Flights Web - Angular 12 baseline

- Angular 12 application with PrimeNG components
- 5 existing Cypress e2e test suites
- SCSS styling with BEM naming convention
- i18n support (10 languages)
- Leaflet map integration
- Complete component hierarchy and routing structure

This baseline will be used for Angular → React migration.
This commit is contained in:
gnezim
2026-04-05 18:47:57 +03:00
commit 0a5ab058a6
34439 changed files with 4408974 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "3.1.8",
"commands": [
"dotnet-ef"
]
}
}
}
+287
View File
@@ -0,0 +1,287 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<SpaRoot>ClientApp\</SpaRoot>
<UserSecretsId>3fa67cb5-d27b-45a4-9d50-39219ff7aa70</UserSecretsId>
<EnableMSDeployAppOffline>true</EnableMSDeployAppOffline>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
<ContainerDevelopmentMode>Regular</ContainerDevelopmentMode>
<EnableSourceControlManagerQueries>true</EnableSourceControlManagerQueries>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\Common\stylecop.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</AdditionalFiles>
<AdditionalFiles Include="..\.globalconfig">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<Compile Remove="ClientApp\dist\**" />
<Compile Remove="ClientApp\src\app\modules\pages\board\components\**" />
<Compile Remove="ClientApp\src\app\shared\components\**" />
<Compile Remove="ClientApp\src\app\shared\constants\**" />
<Content Remove="$(SpaRoot)**" />
<Content Remove="ClientApp\dist\**" />
<Content Remove="ClientApp\src\app\modules\pages\board\components\**" />
<Content Remove="ClientApp\src\app\shared\components\**" />
<Content Remove="ClientApp\src\app\shared\constants\**" />
<EmbeddedResource Remove="ClientApp\dist\**" />
<EmbeddedResource Remove="ClientApp\src\app\modules\pages\board\components\**" />
<EmbeddedResource Remove="ClientApp\src\app\shared\components\**" />
<EmbeddedResource Remove="ClientApp\src\app\shared\constants\**" />
<None Remove="$(SpaRoot)**" />
<None Remove="ClientApp\dist\**" />
<None Remove="ClientApp\src\app\modules\pages\board\components\**" />
<None Remove="ClientApp\src\app\shared\components\**" />
<None Remove="ClientApp\src\app\shared\constants\**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.300" />
<PackageReference Include="AWSSDK.S3" Version="3.7.305.29" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="IdentityModel" Version="6.2.0" />
<PackageReference Include="Iuliia" Version="2.0.4" />
<PackageReference Include="IdentityModel.AspNetCore" Version="3.0.0" />
<PackageReference Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="5.1.0" />
<PackageReference Include="IdentityModel.AspNetCore.ScopeValidation" Version="1.1.1" />
<PackageReference Include="MessagePack" Version="2.5.187" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.16" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.16" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.19" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.15" />
<PackageReference Include="Microsoft.Exchange.WebServices.NETStandard" Version="1.1.1" />
<PackageReference Include="NuGet.Packaging" Version="6.14.0" />
<PackageReference Include="RazorEngineCore" Version="2023.11.1" />
<PackageReference Include="MiaPlaza.ExpressionUtils" Version="1.2.0" />
<PackageReference Include="Miyconst.Cyriller" Version="0.1.1" />
<PackageReference Include="prometheus-net" Version="6.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" Version="2.1.2" />
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageReference Include="Serilog.Sinks.AmazonS3" Version="1.2.3" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="4.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.1" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
<PackageReference Include="System.IO.Packaging" Version="9.0.5" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.2" />
<PackageReference Include="System.Text.Encodings.Web" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageReference Include="Scrutor" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Remove="ClientApp\dist" />
<Folder Include="wwwroot\css\" />
<Folder Include="wwwroot\img\" />
<None Remove="App_Data\key-3822b1fb-84eb-4772-a64f-401ce0307c0b.xml" />
<None Remove="ClientApp\src\app\modules\components\index.ts" />
<None Remove="ClientApp\src\app\modules\flights.module.ts" />
<None Remove="ClientApp\src\app\modules\schedule.module.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\dispatch-mode.enum.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\flight-loaded-state.enum.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\flight-status.enum.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\index.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\language.enum.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\operation-status.enum.ts" />
<None Remove="ClientApp\src\app\shared\enumerators\time-type.enum.ts" />
<None Remove="ClientApp\src\app\shared\models\aircraft.model.ts" />
<None Remove="ClientApp\src\app\shared\models\airport-info-modification.model.ts" />
<None Remove="ClientApp\src\app\shared\models\airport-info.model.ts" />
<None Remove="ClientApp\src\app\shared\models\arrival-station.model.ts" />
<None Remove="ClientApp\src\app\shared\models\arrival-times.model.ts" />
<None Remove="ClientApp\src\app\shared\models\booking-classes.model.ts" />
<None Remove="ClientApp\src\app\shared\models\cross-leg-index.model.ts" />
<None Remove="ClientApp\src\app\shared\models\day-change.model.ts" />
<None Remove="ClientApp\src\app\shared\models\days-of-week.model.ts" />
<None Remove="ClientApp\src\app\shared\models\departure-station.model.ts" />
<None Remove="ClientApp\src\app\shared\models\departure-times.model.ts" />
<None Remove="ClientApp\src\app\shared\models\equipment.model.ts" />
<None Remove="ClientApp\src\app\shared\models\flight-id.model.ts" />
<None Remove="ClientApp\src\app\shared\models\flight-time.model.ts" />
<None Remove="ClientApp\src\app\shared\models\flight-update-signal.model.ts" />
<None Remove="ClientApp\src\app\shared\models\flight.model.ts" />
<None Remove="ClientApp\src\app\shared\models\index.ts" />
<None Remove="ClientApp\src\app\shared\models\onboard-service.model.ts" />
<None Remove="ClientApp\src\app\shared\models\operating-by.model.ts" />
<None Remove="ClientApp\src\app\shared\models\operator.model.ts" />
<None Remove="ClientApp\src\app\shared\models\passenger-info.model.ts" />
<None Remove="ClientApp\src\app\shared\models\passengers.model.ts" />
<None Remove="ClientApp\src\app\shared\models\period-flight-time.model.ts" />
<None Remove="ClientApp\src\app\shared\models\requests\aircraft-type.model.ts" />
<None Remove="ClientApp\src\app\shared\models\requests\search-flight-request.ts" />
<None Remove="ClientApp\src\app\shared\models\schedule-item.model.ts" />
<None Remove="ClientApp\src\app\shared\models\service-class.model.ts" />
<None Remove="ClientApp\src\app\shared\models\service-type.model.ts" />
<None Remove="ClientApp\src\app\shared\models\traffic-restrictions.model.ts" />
<None Remove="ClientApp\src\app\shared\models\type-flight-time.model.ts" />
<None Remove="ClientApp\src\app\shared\services\flights-updater.service.ts" />
<None Remove="ClientApp\src\app\shared\services\http-cancel.service.ts" />
<None Remove="ClientApp\src\app\shared\services\localization.service.ts" />
<None Remove="ClientApp\src\app\shared\services\version.service.ts" />
<Content Include="App_Data\key-3822b1fb-84eb-4772-a64f-401ce0307c0b.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="ClientApp\src\app\test\schedule.json" />
<Folder Include="ClientApp\src\app\modules\pages\schedule\components\" />
<Folder Include="ClientApp\src\assets\img\" />
<Folder Include="ClientApp\src\styles\components\shared\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Aeroflot.Flights.WebApi\Aeroflot.Flights.WebApi.csproj" />
<ProjectReference Include="..\Aeroflot.Flights.DataSpace\Aeroflot.Flights.DataSpace.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Aeroflot.Common.Core">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Common.Core.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Common2">
<HintPath>..\Common\Aeroflot.Common2.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Common2.DataSpace">
<HintPath>..\Common\Aeroflot.Common2.DataSpace.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Common2.Observability.AspNetCore">
<HintPath>..\Common\Aeroflot.Common2.Observability.AspNetCore.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Platform.ApiWrapper">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.ApiWrapper.dll</HintPath>
</Reference>
<!--<Reference Include="Aeroflot.Platform.Common.DataContracts">
<HintPath>..\Common\Aeroflot.Platform.Common.DataContracts.dll</HintPath>
</Reference>-->
<Reference Include="Aeroflot.Platform.Common.DataContracts">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.Common.DataContracts.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Platform.Common.Observability">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.Common.Observability.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Common2.ApiWrapper">
<HintPath>..\Common\Aeroflot.Common2.ApiWrapper.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Platform.Flights.Entities">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.Flights.Entities.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Platform.Handlers.Interfaces">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.Handlers.Interfaces.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Platform.Readers.Interfaces">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Platform.Readers.Interfaces.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Security.Core">
<HintPath>..\Aeroflot.Platform.Libraries\Aeroflot.Security.Core.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Security2">
<HintPath>..\Common\Aeroflot.Security2.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Security2.DataSpace">
<HintPath>..\Common\Aeroflot.Security2.DataSpace.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Security2.Entities">
<HintPath>..\Common\Aeroflot.Security2.Entities.dll</HintPath>
</Reference>
<Reference Include="Aeroflot.Security2.Entities.DataSpace">
<HintPath>..\Common\Aeroflot.Security2.Entities.DataSpace.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="ClientApp\src\app\modules\components\index.ts" />
<TypeScriptCompile Include="ClientApp\src\app\modules\flights.module.ts" />
<TypeScriptCompile Include="ClientApp\src\app\modules\page-modules\schedule.module.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\dispatch-mode.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\flight-loaded-state.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\flight-status.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\index.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\language.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\operation-status.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\enumerators\time-type.enum.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\aircraft.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\airport-info-modification.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\airport-info.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\arrival-station.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\arrival-times.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\booking-classes.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\cross-leg-index.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\day-change.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\days-of-week.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\departure-station.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\departure-times.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\equipment.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\flight-id.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\flight-time.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\flight-update-signal.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\index.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\onboard-service.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\operating-by.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\operator.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\passenger-info.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\passengers.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\period-flight-time.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\aircraft-type.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\requests\search-flight-request.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\leg.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\flight.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\schedule-item.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\service-class.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\service-type.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\traffic-restrictions.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\models\type-flight-time.model.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\services\flights-updater.service.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\services\http-cancel.service.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\services\localization.service.ts" />
<TypeScriptCompile Include="ClientApp\src\app\shared\services\version.service.ts" />
</ItemGroup>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" Condition="'$(DOTNET_RUNNING_IN_CONTAINER)' != 'true'" />
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:prod" Condition="'$(DOTNET_RUNNING_IN_CONTAINER)' != 'true'" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
*.html
+19
View File
@@ -0,0 +1,19 @@
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier', 'plugin:storybook/recommended'],
rules: {
'@typescript-eslint/no-empty-function': 'warn',
'max-len': [
1,
{
code: 80,
ignoreComments: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
},
],
},
};
+2
View File
@@ -0,0 +1,2 @@
node_modules
dist
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": true,
"tabWidth": 4
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/angular',
core: {
builder: '@storybook/builder-webpack5',
},
staticDirs: [{ from: '../src/assets', to: '/assets' }],
};
+17
View File
@@ -0,0 +1,17 @@
import { setCompodocJson } from '@storybook/addon-docs/angular';
import docJson from '../documentation.json';
import { translationsDecorators } from './translations-decorator';
setCompodocJson(docJson);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: { inlineStories: true },
};
export const decorators = [...translationsDecorators];
@@ -0,0 +1,33 @@
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { componentWrapperDecorator, moduleMetadata } from '@storybook/angular';
import { TranslateHttpLoaderFactory } from '../src/app/shared/factories';
@Component({
selector: 'storybook-translate',
template: `<ng-content></ng-content>`,
})
class StorybookTranslateComponent {
constructor(translateService: TranslateService) {
translateService.setDefaultLang('ru');
translateService.use('ru');
}
}
export const translationsDecorators = [
moduleMetadata({
declarations: [StorybookTranslateComponent],
imports: [
HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: TranslateHttpLoaderFactory,
deps: [HttpClient],
},
}),
],
}),
componentWrapperDecorator(StorybookTranslateComponent),
];
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": [
"node"
],
"allowSyntheticDefaultImports": true
},
"exclude": [
"../src/test.ts",
"../src/**/*.spec.ts",
"../projects/**/*.spec.ts"
],
"include": [
"../src/**/*",
"../projects/**/*"
],
"files": [
"./typings.d.ts"
]
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.md' {
const content: string;
export default content;
}
+144
View File
@@ -0,0 +1,144 @@
{
"version": 1,
"projects": {
"flightsapp": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./custom-webpack.config.js"
},
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets",
"src/googledeb8cf566236306b.html",
"src/yandex_02230d34b29eaf63.html",
"src/yandex_1947cc7ca150f934.html"
],
"styles": [
"src/styles.scss",
"src/fonts.scss",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/primeicons/primeicons.css",
"node_modules/leaflet/dist/leaflet.css"
],
"scripts": [
]
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
"testing": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.testing.ts"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
"serviceWorker": false,
"index": {
"input": "src/index.dev.html",
"output": "index.html"
},
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.development.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"proxyConfig": "src/proxy.conf.json"
},
"configurations": {
"production": {
"browserTarget": "flightsapp:build:production"
},
"testing": {
"browserTarget": "flightsapp:build:testing"
},
"development": {
"browserTarget": "flightsapp:build:development"
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
},
"storybook": {
"builder": "@storybook/angular:start-storybook",
"options": {
"browserTarget": "angular-cli:build",
"port": 6006
}
},
"build-storybook": {
"builder": "@storybook/angular:build-storybook",
"options": {
"browserTarget": "angular-cli:build"
}
}
}
}
},
"defaultProject": "flightsapp",
"cli": {
"analytics": false
}
}
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable no-undef */
module.exports = {
output: {
chunkLoadingGlobal: 'webpackJsonp' + Math.random().toString(),
},
};
+27
View File
@@ -0,0 +1,27 @@
{
"integrationFolder": "cypress/integration",
"supportFile": "cypress/support/index.ts",
"videosFolder": "cypress/videos",
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4200",
"screenshotOnRunFailure": false,
"video": false,
"viewportHeight": 768,
"viewportWidth": 1366,
"chromeWebSecurity": false,
"env": {
"browserPermissions": {
"notifications": "allow",
"geolocation": "block",
"camera": "block",
"microphone": "block",
"images": "allow",
"javascript": "allow",
"popups": "ask",
"plugins": "ask",
"cookies": "allow"
}
}
}
@@ -0,0 +1,63 @@
import * as moment from 'moment';
describe('Онлайн табло: Прилет', () => {
const arrivalCity = {
name: 'Анапа',
code: 'AAQ',
latitude: 55.7558,
longitude: 37.62,
};
const today = moment().format('DD.MM.YYYY');
const expectedUrlDateTime = `${moment().format('DDMMYYYY')}-0000-2400`;
beforeEach(() => {
cy.intercept('GET', '**api/flights/v1.1/ru/board**').as('getFlights');
cy.forbidGeolocation();
cy.visit('/');
});
const assertSearchResults = () => {
cy.getByTestId('loader').should('be.visible');
cy.wait('@getFlights').then(() => {
cy.getByTestId('board-search-result').should('be.visible');
cy.getByTestId('flight-result').should('have.length.at.least', 1);
});
};
it(`Должен искать рейсы (${arrivalCity.name} - ручной ввод), открывать корректный URL и детали рейса`, () => {
cy.getByTestId('city-autocomplete-input').type(`${arrivalCity.name}`).type('{enter}');
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('city-code').contains(`${arrivalCity.code}`).should('be.visible');
cy.getByTestId('arrival-search-button').click();
cy.url().should('include', `ru-ru/onlineboard/arrival/${arrivalCity.code}/${expectedUrlDateTime}`);
assertSearchResults();
cy.getByTestId('flight-result')
.first()
.getByTestId('flight-carrier-number')
.should('be.visible')
.getByTestId('flight-company-logo')
.should('be.visible');
cy.getByTestId('flight-details-button').click();
cy.getByTestId('flight-details-number').should('be.visible');
cy.getByTestId('flight-company-logo').should('be.visible');
});
it(`Должен искать рейсы (${arrivalCity.name} - выбор из списка)`, () => {
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('autocomplete-popup-button').click();
cy.getByTestId(`city-name-cell-${arrivalCity.name}`).click();
cy.getByTestId('city-code').contains(`${arrivalCity.code}`).should('be.visible');
cy.getByTestId('arrival-search-button').click();
assertSearchResults();
});
});
@@ -0,0 +1,61 @@
import * as moment from 'moment';
describe('Онлайн табло: Вылет', () => {
const departureCity = {
name: 'Анапа',
code: 'AAQ',
latitude: 44.8857008,
longitude: 37.3199191,
};
const today = moment().format('DD.MM.YYYY');
const expectedUrlDateTime = `${moment().format('DDMMYYYY')}-0000-2400`;
beforeEach(() => {
cy.intercept('GET', '**api/flights/v1.1/ru/board**').as('getFlights');
cy.forbidGeolocation();
cy.visit('/');
cy.getByTestId('departure-filter').click();
});
const assertSearchResults = () => {
cy.getByTestId('loader').should('be.visible');
cy.wait('@getFlights').then(() => {
cy.getByTestId('board-search-result').should('be.visible');
cy.getByTestId('flight-result').should('have.length.at.least', 1);
});
};
it(`Должен искать рейсы (${departureCity.name} - ручной ввод), открывать корректный URL и детали рейса`, () => {
cy.getByTestId('departure-city-input').type(`${departureCity.name}`);
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('city-code').contains(`${departureCity.code}`).should('be.visible');
cy.getByTestId('departure-search-button').click();
cy.url().should('include', `ru-ru/onlineboard/departure/${departureCity.code}/${expectedUrlDateTime}`);
assertSearchResults();
cy.getByTestId('flight-result')
.first()
.getByTestId('flight-carrier-number')
.should('be.visible')
.getByTestId('flight-company-logo')
.should('be.visible');
cy.getByTestId('flight-details-button').click();
cy.getByTestId('flight-details-number').should('be.visible');
cy.getByTestId('flight-company-logo').should('be.visible');
});
it(`Должен искать рейсы (${departureCity.name} - выбор из списка)`, () => {
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('autocomplete-popup-button').click();
cy.getByTestId(`city-name-cell-${departureCity.name}`).click();
cy.getByTestId('city-code').contains(`${departureCity.code}`).should('be.visible');
cy.getByTestId('departure-search-button').click();
assertSearchResults();
});
});
@@ -0,0 +1,58 @@
import * as moment from 'moment';
describe('Онлайн табло: Поиск по маршруту', () => {
const route = {
departureCity: {
name: 'Москва',
code: 'MOW',
latitude: 55.7558,
longitude: 37.62,
},
arrivalCity: {
name: 'Сочи',
code: 'AER',
latitude: 43.58,
longitude: 39.72,
},
};
const today = moment().format('DD.MM.YYYY');
beforeEach(() => {
cy.intercept('GET', '**api/flights/v1.1/ru/board**').as('getFlights');
cy.forbidGeolocation();
cy.visit('/');
cy.getByTestId('route-filter').click();
});
const assertSearchResults = () => {
cy.getByTestId('loader').should('be.visible');
cy.wait('@getFlights').then(() => {
cy.getByTestId('board-search-result').should('be.visible');
cy.getByTestId('flight-result').should('have.length.at.least', 1);
});
};
it(`Должен искать рейсы ${route.departureCity.name} - ${route.arrivalCity.name} , открывать корректный URL и детали рейса`, () => {
cy.getByTestId('route-departure-city-input').type(`${route.departureCity.name}`);
cy.getByTestId('route-arrival-city-input').type(`${route.arrivalCity.name}`);
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('route-arrival-city-input').getByTestId('city-code').contains(`${route.arrivalCity.code}`).should('be.visible');
cy.getByTestId('route-departure-city-input').getByTestId('city-code').contains(`${route.departureCity.code}`).should('be.visible');
cy.getByTestId('route-search-button').click();
assertSearchResults();
cy.getByTestId('flight-result')
.first()
.getByTestId('flight-carrier-number')
.should('be.visible')
.getByTestId('flight-company-logo')
.should('be.visible');
cy.getByTestId('flight-details-button').click();
cy.getByTestId('flight-details-number').should('be.visible');
cy.getByTestId('flight-company-logo').should('be.visible');
});
});
@@ -0,0 +1,44 @@
import * as moment from 'moment';
describe('Онлайн табло: Поиск рейса', () => {
const flightNumber = '0022'; // Москва - Санкт-Петербург
const expectedUrlDateTime = `${moment().format('DDMMYYYY')}`;
const today = moment().format('DD.MM.YYYY');
beforeEach(() => {
cy.intercept('GET', '**api/flights/v1.1/ru/board**').as('getFlights');
cy.visit('/');
cy.getByTestId('flight-filter').click();
});
const assertSearchResults = () => {
cy.getByTestId('loader').should('be.visible');
cy.wait('@getFlights').then(() => {
cy.getByTestId('board-search-result').should('be.visible');
cy.getByTestId('flight-result').should('have.length.at.least', 1);
});
};
it(`Должен искать рейс SU${flightNumber} , открывать корректный URL и детали рейса`, () => {
cy.getByTestId('flight-number-input').type(`${flightNumber}`).type('{enter}');
cy.getByTestId('calendar-input').type(today).type('{enter}');
cy.getByTestId('flight-number-search-button').click();
assertSearchResults();
cy.url().should('include', `ru-ru/onlineboard/flight/SU${flightNumber}/${expectedUrlDateTime}`);
cy.getByTestId('flight-result')
.first()
.getByTestId('flight-carrier-number')
.should('be.visible')
.getByTestId('flight-company-logo')
.should('be.visible');
cy.getByTestId('flight-details-button').click();
cy.getByTestId('flight-details-number').should('be.visible');
cy.getByTestId('flight-company-logo').should('be.visible');
});
});
@@ -0,0 +1,59 @@
describe('Расписание: Поиск по маршруту', () => {
const route = {
departureCity: {
name: 'Москва',
code: 'MOW',
latitude: 55.7558,
longitude: 37.62,
},
arrivalCity: {
name: 'Сочи',
code: 'AER',
latitude: 43.58,
longitude: 39.72,
},
};
beforeEach(() => {
cy.intercept('GET', '**/api/flights/1/ru/schedule**').as('getFlights');
cy.mockGeolocation(route.departureCity);
cy.visit('/ru-ru/schedule');
});
const assertSearchResults = () => {
cy.getByTestId('loader').should('be.visible');
cy.wait('@getFlights').then(() => {
cy.getByTestId('schedule-search-results').should('be.visible');
cy.getByTestId('schedule-search-result').should('have.length.at.least', 1);
});
};
it(`Должен искать рейсы ${route.departureCity.name} - ${route.arrivalCity.name} , открывать корректный URL и детали рейса`, () => {
// cy.getByTestId('schedule-departure-city-input').type(`${route.departureCity.name}`).type('{enter}');
cy.getByTestId('schedule-arrival-city-input').type(`${route.arrivalCity.name}`).type('{enter}');
cy.wait(200);
cy.getByTestId('schedule-arrival-city-input').getByTestId('city-code').contains(`${route.arrivalCity.code}`).should('be.visible');
cy.getByTestId('schedule-departure-city-input')
.getByTestId('city-code')
.contains(`${route.departureCity.code}`)
.should('be.visible');
cy.getByTestId('schedule-search-button').click();
assertSearchResults();
cy.getByTestId('schedule-search-result')
.first()
.getByTestId('flight-number')
.should('be.visible')
.getByTestId('flight-carrier')
.should('be.visible');
cy.getByTestId('flight-details-button').click({ force: true });
cy.getByTestId('flight-details-number').should('be.visible');
cy.getByTestId('flight-company-logo').should('be.visible');
});
});
+10
View File
@@ -0,0 +1,10 @@
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api
import { cypressBrowserPermissionsPlugin } from 'cypress-browser-permissions';
module.exports = (on, config) => {
config = cypressBrowserPermissionsPlugin(on, config);
return config;
};
+73
View File
@@ -0,0 +1,73 @@
/// <reference types="." />
// ***********************************************
// This example namespace declaration will help
// with Intellisense and code completion in your
// IDE or Text Editor.
// ***********************************************
// declare namespace Cypress {
// interface Chainable<Subject = any> {
// customCommand(param: any): typeof customCommand;
// }
// }
//
// function customCommand(param: any): void {
// console.warn(param);
// }
//
// NOTE: You can use it like so:
// Cypress.Commands.add('customCommand', customCommand);
//
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('getByTestId', (id: string, timeout = 8000) => {
return cy.get(`[data-testid="${id}"]`, { timeout });
});
Cypress.Commands.add('mockGeolocation', ({ latitude, longitude }) => {
cy.on('window:before:load', (window) => {
const callback = (cb) => {
return cb({ coords: { latitude, longitude } });
};
cy.stub(window.navigator.geolocation, 'getCurrentPosition').callsFake(callback);
cy.stub(window.navigator.geolocation, 'watchPosition').callsFake(callback);
});
});
Cypress.Commands.add('forbidGeolocation', () => {
cy.on('window:before:load', (window) => {
const callback = (_, error) => {
return error({});
};
cy.stub(window.navigator.geolocation, 'getCurrentPosition').callsFake(callback);
cy.stub(window.navigator.geolocation, 'watchPosition').callsFake(callback);
});
});
+8
View File
@@ -0,0 +1,8 @@
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
getByTestId(id: string, timeout?: number): Chainable;
mockGeolocation({ latitude, longitude }): void;
forbidGeolocation();
}
}
+17
View File
@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// When a command from ./commands is ready to use, import with `import './commands'` syntax
import './commands';
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"include": ["**/*.ts"],
"compilerOptions": {
"sourceMap": true,
"types": ["cypress"]
}
}
+4636
View File
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
{
"pipes": [],
"interfaces": [],
"injectables": [],
"guards": [],
"interceptors": [],
"classes": [],
"directives": [],
"components": [],
"modules": [],
"miscellaneous": {
"variables": [
{
"name": "platform",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "",
"defaultValue": "platformBrowserDynamic()"
}
],
"functions": [],
"typealiases": [],
"enumerations": [],
"groupedVariables": {
"src/main.ts": [
{
"name": "platform",
"ctype": "miscellaneous",
"subtype": "variable",
"file": "src/main.ts",
"deprecated": false,
"deprecationMessage": "",
"type": "",
"defaultValue": "platformBrowserDynamic()"
}
]
},
"groupedFunctions": {},
"groupedEnumerations": {},
"groupedTypeAliases": {}
},
"routes": [],
"coverage": {
"count": 0,
"status": "low",
"files": [
{
"filePath": "src/main.ts",
"type": "variable",
"linktype": "miscellaneous",
"linksubtype": "variable",
"name": "platform",
"coveragePercent": 0,
"coverageCount": "0/1",
"status": "low"
}
]
}
}
+19
View File
@@ -0,0 +1,19 @@
// direct
- MOW -> PRG (direct)
// multileg - many legs
- MOW -> TKY -> PRG
// connected (direct + direct) - many legs
- MOW -> TKY
TKY -> PRG
// connected (multileg + direct) - many legs
- MOW -> TKY -> RYZ
RYZ -> PRG
// connected (multileg + multileg) - many legs
- MOW -> TKY -> RYZ
RYZ -> PRG -> LSB
+45
View File
@@ -0,0 +1,45 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-undef */
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
require('karma-teamcity-reporter'),
require('karma-phantomjs-launcher')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/test'),
subdir: '.',
reporters: [{ type: 'html' }, { type: 'text-summary' }],
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true,
});
};
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../acorn/bin/acorn
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../cypress/bin/cypress
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../esbuild/bin/esbuild
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../eslint/bin/eslint.js
+1
View File
@@ -0,0 +1 @@
../eslint-config-prettier/bin/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../esprima/bin/esparse.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../esprima/bin/esvalidate.js
+1
View File
@@ -0,0 +1 @@
../extract-zip/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../is-docker/cli.js
+1
View File
@@ -0,0 +1 @@
../is-inside-container/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../jiti/lib/jiti-cli.mjs
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../js-yaml/bin/js-yaml.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../json5/lib/cli.js
+1
View File
@@ -0,0 +1 @@
../loose-envify/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../lz-string/bin/bin.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../which/bin/node-which
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../@babel/parser/bin/babel-parser.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../@playwright/test/cli.js
+1
View File
@@ -0,0 +1 @@
../playwright-core/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../prettier/bin/prettier.cjs
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../resolve/bin/resolve
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../rimraf/bin.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../rollup/dist/bin/rollup
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../@rsbuild/core/bin/rsbuild.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../sass/sass.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../semver/bin/semver.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../sshpk/bin/sshpk-conv
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../sshpk/bin/sshpk-sign
+1
View File
@@ -0,0 +1 @@
../sshpk/bin/sshpk-verify
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../storybook/dist/bin/dispatcher.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../tldts/bin/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../tree-kill/cli.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../typescript/bin/tsc
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../typescript/bin/tsserver
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../uuid/dist/bin/uuid
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../vite/bin/vite.js
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../vite-node/vite-node.mjs
Generated Vendored Symlink
+1
View File
@@ -0,0 +1 @@
../vitest/vitest.mjs
+1
View File
@@ -0,0 +1 @@
../why-is-node-running/cli.js
+9099
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
(The MIT License)
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2022 Jean-Philippe Zolesio <holblin@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+146
View File
@@ -0,0 +1,146 @@
# @adobe/css-tools
> A modern CSS parser and stringifier with TypeScript support
[![npm version](https://badge.fury.io/js/%40adobe%2Fcss-tools.svg)](https://badge.fury.io/js/%40adobe%2Fcss-tools)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Parse CSS into an Abstract Syntax Tree (AST) and convert it back to CSS with configurable formatting. Built with TypeScript for type safety and modern JavaScript features.
## Install
```bash
npm install @adobe/css-tools
```
## Usage
```js
import { parse, stringify } from '@adobe/css-tools'
// Parse CSS to AST
const ast = parse('body { font-size: 12px; }')
// Stringify AST back to CSS
const css = stringify(ast)
// => "body { font-size: 12px; }"
// Pretty print with custom indentation
const formatted = stringify(ast, { indent: ' ' })
// => "body {\n font-size: 12px;\n}"
// Minify output
const minified = stringify(ast, { compress: true })
// => "body{font-size:12px}"
```
## API
### `parse(code, options?)`
Parses CSS code and returns an Abstract Syntax Tree (AST).
**Parameters:**
- `code` (string) - The CSS code to parse
- `options` (object, optional) - Parsing options
- `silent` (boolean) - Silently fail on parse errors instead of throwing
- `source` (string) - File path for better error reporting
**Returns:** `CssStylesheetAST` - The parsed CSS as an AST
### `stringify(ast, options?)`
Converts a CSS AST back to CSS string with configurable formatting.
**Parameters:**
- `ast` (CssStylesheetAST) - The CSS AST to stringify
- `options` (object, optional) - Stringification options
- `indent` (string) - Indentation string (default: `' '`)
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
**Returns:** `string` - The formatted CSS string
## Features
- **Complete CSS Support**: All standard CSS features including selectors, properties, values, at-rules, and comments
- **TypeScript Support**: Full type definitions for all AST nodes and functions
- **Error Handling**: Configurable error handling with detailed position information
- **Formatting Options**: Pretty print, minify, or custom formatting
- **Performance Optimized**: Efficient parsing and stringification for large CSS files
- **Source Maps**: Track original source positions for debugging and tooling
### Supported CSS Features
- **Selectors**: Element, class, ID, attribute, pseudo-class, pseudo-element selectors
- **Properties**: All standard CSS properties and custom properties
- **Values**: Colors, lengths, percentages, functions, calc(), etc.
- **At-rules**: @media, @keyframes, @import, @charset, @namespace, @font-face, @page, @document, @supports, @container, @layer, @starting-style, @host, @custom-media
- **Comments**: Both /* */ and // comments
- **Whitespace**: Preserves formatting information
- **Vendor prefixes**: Supports vendor-prefixed at-rules and properties
- **Nested rules**: Media queries, supports, containers, etc.
- **Complex selectors**: Combinators, pseudo-selectors, attribute selectors
## Examples
### Error Handling
```js
import { parse } from '@adobe/css-tools'
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`
// Parse with silent error handling
const result = parse(malformedCss, { silent: true })
// Check for parsing errors
if (result.stylesheet.parsingErrors) {
console.log('Parsing errors:', result.stylesheet.parsingErrors.length)
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`)
})
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length)
```
### Source Tracking
```js
import { parse } from '@adobe/css-tools'
const css = 'body { color: red; }'
const ast = parse(css, { source: 'styles.css' })
// Position information is available
const rule = ast.stylesheet.rules[0]
console.log(rule.position?.source) // "styles.css"
console.log(rule.position?.start) // { line: 1, column: 1 }
console.log(rule.position?.end) // { line: 1, column: 20 }
```
For more examples, see the [Examples documentation](docs/EXAMPLES.md).
## Performance
The library is optimized for performance and can handle large CSS files efficiently. For benchmarking information, see the `benchmark/` directory in the source code.
## Documentation
- [API Reference](docs/API.md) - Complete API documentation
- [AST Structure](docs/AST.md) - Detailed AST node types and structure
- [Examples](docs/EXAMPLES.md) - Comprehensive usage examples
- [Changelog](docs/CHANGELOG.md) - Version history and changes
## Background
This is a fork of the npm `css` package, maintained by Adobe with modern improvements including TypeScript support, enhanced performance, and security updates. It provides a robust foundation for CSS tooling, preprocessing, and analysis.
## License
[MIT](LICENSE)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+176
View File
@@ -0,0 +1,176 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
container = "container",
charset = "charset",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
startingStyle = "starting-style",
supports = "supports"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssSupportsAST | CssStartingStyleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssRuleAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST };
+176
View File
@@ -0,0 +1,176 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
container = "container",
charset = "charset",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
startingStyle = "starting-style",
supports = "supports"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssSupportsAST | CssStartingStyleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssRuleAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+176
View File
@@ -0,0 +1,176 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
container = "container",
charset = "charset",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
startingStyle = "starting-style",
supports = "supports"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssSupportsAST | CssStartingStyleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssRuleAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+314
View File
@@ -0,0 +1,314 @@
# API Reference
## Overview
`@adobe/css-tools` provides a modern CSS parser and stringifier with comprehensive TypeScript support. It can parse CSS into an Abstract Syntax Tree (AST) and convert the AST back to CSS with various formatting options.
## Installation
```bash
npm install @adobe/css-tools
```
## Core Functions
### `parse(code, options?)`
Parses CSS code and returns an Abstract Syntax Tree (AST).
#### Parameters
- `code` (string) - The CSS code to parse
- `options` (object, optional) - Parsing options
- `silent` (boolean) - Silently fail on parse errors instead of throwing. When `true`, errors are collected in `ast.stylesheet.parsingErrors`
- `source` (string) - File path for better error reporting
#### Returns
- `CssStylesheetAST` - The parsed CSS as an AST
#### Example
```javascript
import { parse } from '@adobe/css-tools';
const css = `
.example {
color: red;
font-size: 16px;
}
`;
const ast = parse(css);
console.log(ast.stylesheet.rules);
```
### `stringify(ast, options?)`
Converts a CSS AST back to CSS string with configurable formatting.
#### Parameters
- `ast` (CssStylesheetAST) - The CSS AST to stringify
- `options` (CompilerOptions, optional) - Stringification options
- `indent` (string) - Indentation string (default: `' '`)
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
#### Returns
- `string` - The formatted CSS string
#### Example
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = '.example { color: red; }';
const ast = parse(css);
// Pretty print
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// .example {
// color: red;
// }
// Compressed
const minified = stringify(ast, { compress: true });
console.log(minified);
// Output: .example{color:red}
```
## Type Definitions
### Core Types
#### `CssStylesheetAST`
The root AST node representing a complete CSS stylesheet.
```typescript
type CssStylesheetAST = {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: CssRuleAST[];
parsingErrors?: CssParseError[];
};
};
```
#### `CssRuleAST`
Represents a CSS rule (selector + declarations).
```typescript
type CssRuleAST = {
type: CssTypes.rule;
selectors: string[];
declarations: CssDeclarationAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssDeclarationAST`
Represents a CSS property declaration.
```typescript
type CssDeclarationAST = {
type: CssTypes.declaration;
property: string;
value: string;
position?: CssPosition;
parent?: CssRuleAST;
};
```
#### `CssMediaAST`
Represents a CSS @media rule.
```typescript
type CssMediaAST = {
type: CssTypes.media;
media: string;
rules: CssRuleAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssKeyframesAST`
Represents a CSS @keyframes rule.
```typescript
type CssKeyframesAST = {
type: CssTypes.keyframes;
name: string;
keyframes: CssKeyframeAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssPosition`
Represents source position information.
```typescript
type CssPosition = {
source?: string;
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
};
```
#### `CssParseError`
Represents a parsing error.
```typescript
type CssParseError = {
message: string;
reason: string;
filename?: string;
line: number;
column: number;
source?: string;
};
```
### Compiler Options
#### `CompilerOptions`
Options for the stringifier.
```typescript
type CompilerOptions = {
indent?: string; // Default: ' '
compress?: boolean; // Default: false
};
```
## Error Handling
### Silent Parsing
When parsing malformed CSS, you can use the `silent` option to collect errors instead of throwing:
```javascript
import { parse } from '@adobe/css-tools';
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`;
const result = parse(malformedCss, { silent: true });
if (result.stylesheet.parsingErrors) {
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`);
});
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length);
```
### Source Tracking
Enable source tracking for better error reporting:
```javascript
import { parse } from '@adobe/css-tools';
const css = 'body { color: red; }';
const ast = parse(css, { source: 'styles.css' });
const rule = ast.stylesheet.rules[0];
console.log(rule.position?.source); // "styles.css"
console.log(rule.position?.start); // { line: 1, column: 1 }
console.log(rule.position?.end); // { line: 1, column: 20 }
```
## Advanced Usage
### Working with At-Rules
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
@media (max-width: 768px) {
.container {
padding: 10px;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`;
const ast = parse(css);
// Access media rules
const mediaRule = ast.stylesheet.rules.find(rule => rule.type === 'media');
console.log(mediaRule.media); // "(max-width: 768px)"
// Access keyframes
const keyframesRule = ast.stylesheet.rules.find(rule => rule.type === 'keyframes');
console.log(keyframesRule.name); // "fadeIn"
```
### Custom Formatting
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = '.example{color:red;font-size:16px}';
const ast = parse(css);
// Custom indentation
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// .example {
// color: red;
// font-size: 16px;
// }
// Compressed with no spaces
const compressed = stringify(ast, { compress: true });
console.log(compressed);
// Output: .example{color:red;font-size:16px}
```
## TypeScript Integration
The library provides comprehensive TypeScript support with full type definitions for all AST nodes and functions:
```typescript
import { parse, stringify, type CssStylesheetAST } from '@adobe/css-tools';
const css: string = '.example { color: red; }';
const ast: CssStylesheetAST = parse(css);
const output: string = stringify(ast);
```
## Performance Considerations
- The parser is optimized for large CSS files
- AST nodes are lightweight and memory-efficient
- Stringification is fast and supports streaming for large outputs
- Consider using `compress: true` for production builds to reduce file size
## Browser Support
The library works in all modern browsers and Node.js environments. For older environments, you may need to use a bundler with appropriate polyfills.
+366
View File
@@ -0,0 +1,366 @@
# Abstract Syntax Tree (AST)
## Overview
The AST represents CSS as a tree structure where each node has a specific type and properties. All nodes share common properties and have type-specific properties.
## Common Properties
All AST nodes have these properties:
### `type`
The node type as a string. See [Node Types](#node-types) for all possible values.
### `position` (optional)
Position information for the node in the source code:
```typescript
{
start: { line: number; column: number };
end: { line: number; column: number };
source?: string;
}
```
### `parent` (optional)
Reference to the parent node in the AST.
## Node Types
### `stylesheet`
The root node representing an entire CSS document.
**Properties:**
- `stylesheet.source` (optional): Source file path
- `stylesheet.rules`: Array of top-level rules
- `stylesheet.parsingErrors` (optional): Array of parse errors when `silent` option is used
**Example:**
```json
{
"type": "stylesheet",
"stylesheet": {
"rules": [
// ... other nodes
]
}
}
```
### `rule`
A CSS rule with selectors and declarations.
**Properties:**
- `selectors`: Array of CSS selectors as strings
- `declarations`: Array of declarations and comments
**Example:**
```json
{
"type": "rule",
"selectors": ["body", "html"],
"declarations": [
{
"type": "declaration",
"property": "color",
"value": "red"
}
]
}
```
### `declaration`
A CSS property declaration.
**Properties:**
- `property`: The CSS property name
- `value`: The CSS property value as a string
**Example:**
```json
{
"type": "declaration",
"property": "background-color",
"value": "#ffffff"
}
```
### `comment`
A CSS comment.
**Properties:**
- `comment`: The comment text (without `/*` and `*/`)
**Example:**
```json
{
"type": "comment",
"comment": " This is a comment "
}
```
### `media`
A `@media` rule.
**Properties:**
- `media`: The media query string
- `rules`: Array of rules within the media block
**Example:**
```json
{
"type": "media",
"media": "screen and (max-width: 768px)",
"rules": [
// ... nested rules
]
}
```
### `keyframes`
A `@keyframes` rule.
**Properties:**
- `name`: The keyframes name
- `vendor` (optional): Vendor prefix (e.g., "-webkit-")
- `keyframes`: Array of keyframe rules and comments
**Example:**
```json
{
"type": "keyframes",
"name": "fade",
"keyframes": [
{
"type": "keyframe",
"values": ["from"],
"declarations": [
{
"type": "declaration",
"property": "opacity",
"value": "0"
}
]
}
]
}
```
### `keyframe`
A keyframe within a `@keyframes` rule.
**Properties:**
- `values`: Array of keyframe selectors (e.g., `["from"]`, `["to"]`, `["50%"]`)
- `declarations`: Array of declarations and comments
### `import`
An `@import` rule.
**Properties:**
- `import`: The import string (URL or media query)
**Example:**
```json
{
"type": "import",
"import": "url('styles.css')"
}
```
### `charset`
A `@charset` rule.
**Properties:**
- `charset`: The character encoding
**Example:**
```json
{
"type": "charset",
"charset": "utf-8"
}
```
### `namespace`
A `@namespace` rule.
**Properties:**
- `namespace`: The namespace declaration
**Example:**
```json
{
"type": "namespace",
"namespace": "url(http://www.w3.org/1999/xhtml)"
}
```
### `supports`
A `@supports` rule.
**Properties:**
- `supports`: The supports condition
- `rules`: Array of rules within the supports block
**Example:**
```json
{
"type": "supports",
"supports": "(display: grid)",
"rules": [
// ... nested rules
]
}
```
### `document`
A `@document` rule.
**Properties:**
- `document`: The document condition
- `vendor` (optional): Vendor prefix
- `rules`: Array of rules within the document block
### `page`
A `@page` rule.
**Properties:**
- `selectors`: Array of page selectors
- `declarations`: Array of declarations and comments
### `font-face`
A `@font-face` rule.
**Properties:**
- `declarations`: Array of font declarations and comments
### `host`
A `:host` rule.
**Properties:**
- `rules`: Array of rules within the host block
### `container`
A `@container` rule.
**Properties:**
- `container`: The container query
- `rules`: Array of rules within the container block
### `layer`
A `@layer` rule.
**Properties:**
- `layer`: The layer name
- `rules` (optional): Array of rules within the layer block
### `custom-media`
A `@custom-media` rule.
**Properties:**
- `name`: The custom media query name
- `media`: The media query definition
### `starting-style`
A `@starting-style` rule.
**Properties:**
- `rules`: Array of rules within the starting-style block
## Type Hierarchy
The AST nodes are organized in the following hierarchy:
- `CssStylesheetAST` - Root node
- `CssAtRuleAST` - Union of all at-rule and rule nodes
- `CssAllNodesAST` - Union of all possible node types
## Working with the AST
### Traversing Nodes
```typescript
import { parse, CssStylesheetAST } from '@adobe/css-tools';
const ast: CssStylesheetAST = parse('body { color: red; }');
// Access top-level rules
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'rule') {
console.log('Selectors:', rule.selectors);
rule.declarations.forEach(decl => {
if (decl.type === 'declaration') {
console.log(`${decl.property}: ${decl.value}`);
}
});
}
});
```
### Modifying Nodes
```typescript
// Add a new declaration
const newDecl = {
type: 'declaration' as const,
property: 'font-size',
value: '16px'
};
// Find a rule and add the declaration
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'rule' && rule.selectors.includes('body')) {
rule.declarations.push(newDecl);
}
});
```
### Error Handling
When parsing with the `silent` option, errors are collected in the AST:
```typescript
const ast = parse('invalid css {', { silent: true });
if (ast.stylesheet.parsingErrors) {
ast.stylesheet.parsingErrors.forEach(error => {
console.error('Parse error:', error.message);
});
}
```
## Position Information
Position information is available on most nodes and includes:
- `start.line` and `start.column`: Beginning of the node
- `end.line` and `end.column`: End of the node
- `source`: Source file path (if provided during parsing)
This is useful for:
- Error reporting
- Source mapping
- Code analysis tools
- IDE integration
+191
View File
@@ -0,0 +1,191 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.4.4] - 2025-07-22
### Changed
- Switch from yarn to npm for package management
- Switch from eslint to biome for code formatting and linting
- Reformat codebase to comply with biome recommendations
- Switch from webpack to rollup for bundling
### Fixed
- Fix module exports to ensure proper compatibility with bundlers
- Add validation check to prevent future export issues
## [4.4.3] - 2025-05-15
### Security
- Fix polynomial regular expression vulnerability on uncontrolled data
- Refactor code to enable GitHub security static analysis
### Performance
- Improve parsing performance with minor optimizations
- Replace regex patterns with string search (indexOf-based) for better performance
### Added
- Add new utility functions with comprehensive unit tests
- Add improved formatting for CSS Grid template areas (#283 by @jogibear9988)
### Fixed
- Fix TypeScript error with ConstructorParameters in Parcel bundler (#444)
## [4.4.2] - 2025-02-12
### Fixed
- Fix regular expression for parsing quoted values in parentheses
## [4.4.0] - 2024-06-05
### Added
- Add support for CSS `@starting-style` at-rule (#319)
## [4.3.3] - 2024-01-24
### Changed
- Update package export configuration (#271)
## [4.3.2] - 2023-11-28
### Security
- Fix ReDoS vulnerability with crafted CSS strings - CVE-2023-48631
### Fixed
- Fix parsing issues with `:is()` and nested `:nth-child()` selectors (#211)
## [4.3.1] - 2023-03-14
### Security
- Fix ReDoS vulnerability with crafted CSS strings - CVE-2023-26364
## [4.3.0] - 2023-03-07
### Changed
- Update build toolchain and dependencies
- Update package exports configuration and file structure
## [4.2.0] - 2023-02-21
### Added
- Add support for CSS `@container` at-rule
- Add support for CSS `@layer` at-rule
## [4.1.0] - 2023-01-25
### Added
- Add support for ES Modules (ESM)
## [4.0.2] - 2023-01-12
### Fixed
- Fix `@import` parsing when URL contains semicolons (#71)
- Fix regression in selector parsing for attribute selectors (#77)
## [4.0.1] - 2022-08-03
### Fixed
- Fix `globalThis` configuration for webpack to enable UMD module usage in Node.js environments
## [4.0.0] - 2022-06-09
### Changed
- Fork from original css library to Adobe's `@adobe/css-tools` package
- Convert codebase from JavaScript to TypeScript
- Improve parsing performance by approximately 25%
- Update all dependencies to latest versions
- Remove source map support
---
## Legacy Versions (Pre-Adobe Fork)
## [3.0.0] - 2020-07-01
### Changed
- Bump major version due to major dependency updates and Node.js version requirement changes
## [2.2.1] - 2015-06-17
### Fixed
- Fix parsing of escaped quotes in quoted strings
## [2.2.0] - 2015-02-18
### Added
- Add `parsingErrors` property to list errors when parsing with `silent: true`
- Accept EOL characters and all whitespace characters in at-rules such as `@media`
## [2.1.0] - 2014-08-05
### Added
- Add `inputSourcemaps` option to disable input source map processing
- Add `sourcemap: 'generator'` option to return the `SourceMapGenerator` object
- Use `inherits` package for inheritance (fixes browser compatibility issues)
### Changed
- Change error message format and add `.reason` property to error objects
## [2.0.0] - 2014-06-18
### Added
- Add non-enumerable parent reference to each AST node
- Add support for `@custom-media`, `@host`, and `@font-face` at-rules
- Allow commas inside selector functions
- Allow empty property values
- Add `node.position.content` property
- Integrate css-parse and css-stringify libraries
- Apply original source maps from source files
### Changed
- Change default `options.position` value to `true`
- Remove comments from properties and values
### Removed
- Drop Component(1) support
### Fixed
- Fix assertion errors when selectors are missing
## [1.6.1] - 2014-01-02
### Fixed
- Fix component.json configuration
## [1.6.0] - 2013-12-21
### Changed
- Update dependencies
## [1.5.0] - 2013-12-03
### Changed
- Update dependencies
## [1.1.0] - 2013-04-04
### Changed
- Update dependencies
## [1.0.7] - 2012-11-21
### Fixed
- Fix component.json configuration
## [1.0.4] - 2012-11-15
### Changed
- Update css-stringify dependency
## [1.0.3] - 2012-09-01
### Added
- Add Component support
## [0.0.1] - 2010-01-03
### Added
- Initial release
+450
View File
@@ -0,0 +1,450 @@
# Usage Examples
## Basic Usage
### Parsing CSS
```javascript
import { parse } from '@adobe/css-tools';
// Basic CSS parsing
const css = `
body {
font-size: 12px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
`;
const ast = parse(css);
console.log(ast);
```
### Stringifying AST
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = 'body { font-size: 12px; color: #333; }';
const ast = parse(css);
// Convert back to CSS
const output = stringify(ast);
console.log(output); // "body { font-size: 12px; color: #333; }"
```
## Advanced Parsing Options
### Source Tracking
```javascript
import { parse } from '@adobe/css-tools';
const css = 'body { color: red; }';
const ast = parse(css, { source: 'styles.css' });
// Position information is available
const rule = ast.stylesheet.rules[0];
console.log(rule.position?.source); // "styles.css"
console.log(rule.position?.start); // { line: 1, column: 1 }
console.log(rule.position?.end); // { line: 1, column: 20 }
```
### Silent Error Handling
```javascript
import { parse } from '@adobe/css-tools';
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`;
// Parse with silent error handling
const result = parse(malformedCss, { silent: true });
// Check for parsing errors
if (result.stylesheet.parsingErrors) {
console.log('Parsing errors:', result.stylesheet.parsingErrors.length);
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`);
});
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length);
```
## AST Structure Examples
### Basic Rule
```javascript
import { parse } from '@adobe/css-tools';
const css = `
.header {
background: #f0f0f0;
padding: 20px;
border-bottom: 1px solid #ccc;
}
`;
const ast = parse(css);
const rule = ast.stylesheet.rules[0];
console.log(rule.type); // "rule"
console.log(rule.selectors); // [".header"]
console.log(rule.declarations.length); // 3
rule.declarations.forEach(decl => {
console.log(`${decl.property}: ${decl.value}`);
});
// Output:
// background: #f0f0f0
// padding: 20px
// border-bottom: 1px solid #ccc
```
### Media Queries
```javascript
import { parse } from '@adobe/css-tools';
const css = `
@media screen and (max-width: 768px) {
.container {
padding: 10px;
}
.sidebar {
display: none;
}
}
`;
const ast = parse(css);
const mediaRule = ast.stylesheet.rules[0];
console.log(mediaRule.type); // "media"
console.log(mediaRule.media); // "screen and (max-width: 768px)"
console.log(mediaRule.rules.length); // 2
```
### Keyframes
```javascript
import { parse } from '@adobe/css-tools';
const css = `
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
const ast = parse(css);
const keyframesRule = ast.stylesheet.rules[0];
console.log(keyframesRule.type); // "keyframes"
console.log(keyframesRule.name); // "fadeIn"
console.log(keyframesRule.keyframes.length); // 2
keyframesRule.keyframes.forEach(keyframe => {
console.log(`Keyframe: ${keyframe.values.join(', ')}`);
keyframe.declarations.forEach(decl => {
console.log(` ${decl.property}: ${decl.value}`);
});
});
```
### Comments
```javascript
import { parse } from '@adobe/css-tools';
const css = `
/* Header styles */
.header {
background: red; /* Fallback color */
}
/* Footer styles */
.footer {
background: blue;
}
`;
const ast = parse(css);
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'comment') {
console.log(`Comment: ${rule.comment}`);
} else if (rule.type === 'rule') {
console.log(`Rule: ${rule.selectors.join(', ')}`);
rule.declarations.forEach(decl => {
if (decl.type === 'comment') {
console.log(` Comment: ${decl.comment}`);
} else {
console.log(` ${decl.property}: ${decl.value}`);
}
});
}
});
```
## Stringifying Options
### Compressed Output
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
body {
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
}
`;
const ast = parse(css);
// Compressed output
const compressed = stringify(ast, { compress: true });
console.log(compressed);
// Output: "body{font-size:12px;color:#333;margin:0;padding:0}"
```
### Custom Indentation
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = 'body { font-size: 12px; color: #333; }';
const ast = parse(css);
// Custom indentation
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// body {
// font-size: 12px;
// color: #333;
// }
```
## Working with Complex CSS
### Nested Rules and At-Rules
```javascript
import { parse, stringify } from '@adobe/css-tools';
const complexCss = `
@import url('https://fonts.googleapis.com/css2?family=Roboto');
@charset "UTF-8";
@media print {
body {
font-size: 12pt;
}
}
@supports (display: grid) {
.grid {
display: grid;
}
}
@keyframes slideIn {
0% { transform: translateX(-100%); }
100% { transform: translateX(0); }
}
@font-face {
font-family: 'CustomFont';
src: url('custom-font.woff2') format('woff2');
}
`;
const ast = parse(complexCss);
ast.stylesheet.rules.forEach(rule => {
switch (rule.type) {
case 'import':
console.log(`Import: ${rule.import}`);
break;
case 'charset':
console.log(`Charset: ${rule.charset}`);
break;
case 'media':
console.log(`Media query: ${rule.media}`);
break;
case 'supports':
console.log(`Supports: ${rule.supports}`);
break;
case 'keyframes':
console.log(`Keyframes: ${rule.name}`);
break;
case 'font-face':
console.log('Font-face rule');
break;
}
});
```
### Manipulating the AST
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
.button {
background: blue;
color: white;
padding: 10px;
}
`;
const ast = parse(css);
const rule = ast.stylesheet.rules[0];
// Add a new declaration
rule.declarations.push({
type: 'declaration',
property: 'border-radius',
value: '5px'
});
// Modify existing declaration
const backgroundDecl = rule.declarations.find(d => d.property === 'background');
if (backgroundDecl) {
backgroundDecl.value = 'red';
}
// Add a new selector
rule.selectors.push('.btn');
const modifiedCss = stringify(ast);
console.log(modifiedCss);
```
## Error Handling
### Catching Parse Errors
```javascript
import { parse, CssParseError } from '@adobe/css-tools';
try {
const ast = parse('body { color: red; } { invalid }');
} catch (error) {
if (error instanceof CssParseError) {
console.log(`Parse error at line ${error.line}, column ${error.column}:`);
console.log(error.message);
console.log(`Source: ${error.filename}`);
}
}
```
### Working with Silent Errors
```javascript
import { parse } from '@adobe/css-tools';
const problematicCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
.another { border: 1px solid; } /* Missing closing brace */
`;
const result = parse(problematicCss, {
silent: true,
source: 'problematic.css'
});
// Process valid rules
const validRules = result.stylesheet.rules.filter(rule => rule.type === 'rule');
console.log(`Found ${validRules.length} valid rules`);
// Log errors for debugging
if (result.stylesheet.parsingErrors) {
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error: ${error.message} at line ${error.line}`);
});
}
```
### CSS Minification
```javascript
import { parse, stringify } from '@adobe/css-tools';
function minifyCSS(css) {
const ast = parse(css);
return stringify(ast, { compress: true });
}
const css = `
body {
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
`;
const minified = minifyCSS(css);
console.log(minified);
// Output: "body{font-size:12px;color:#333;margin:0;padding:0}.container{max-width:1200px;margin:0 auto}"
```
### CSS Validation
```javascript
import { parse } from '@adobe/css-tools';
function validateCSS(css, filename = 'unknown') {
try {
const ast = parse(css, { source: filename });
return {
valid: true,
rules: ast.stylesheet.rules.length,
errors: []
};
} catch (error) {
return {
valid: false,
rules: 0,
errors: [{
message: error.message,
line: error.line,
column: error.column,
source: error.filename
}]
};
}
}
const result = validateCSS('body { color: red; } { invalid }', 'test.css');
console.log(result);
```
+82
View File
@@ -0,0 +1,82 @@
{
"name": "@adobe/css-tools",
"version": "4.4.4",
"description": "A modern CSS parser and stringifier with TypeScript support",
"source": "src/index.ts",
"main": "./dist/cjs/adobe-css-tools.cjs",
"module": "./dist/esm/adobe-css-tools.mjs",
"browser": "./dist/umd/adobe-css-tools.js",
"sideEffects": false,
"exports": {
".": {
"import": "./dist/esm/adobe-css-tools.mjs",
"require": "./dist/cjs/adobe-css-tools.cjs"
},
"./package.json": "./package.json"
},
"types": "./dist/esm/adobe-css-tools.d.mts",
"type": "module",
"files": [
"dist/cjs",
"dist/esm",
"dist/umd",
"README.md",
"docs/"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@biomejs/biome": "^2.1.2",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.4",
"@types/benchmark": "^2.1.1",
"@types/bytes": "^3.1.5",
"@types/jest": "^29.5.14",
"@types/node": "^24.0.3",
"benchmark": "^2.1.4",
"bytes": "^3.1.0",
"jest": "^29.7.0",
"rollup": "^4.45.1",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.1.1",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
},
"scripts": {
"benchmark": "npm run build && node benchmark/index.mjs",
"test": "jest",
"clean": "rm -rf ./dist",
"build": "rollup -c",
"fix": "biome check --write",
"check-package-bundle": "attw --pack .",
"lint": "biome check",
"prebuild": "npm run clean",
"prepack": "rollup -c",
"posttest": "npm run lint",
"postbuild": "tar -czf dist/archive.tgz LICENSE README.md dist/cjs dist/esm dist/umd docs package.json && npx attw --format ascii dist/archive.tgz && rm dist/archive.tgz"
},
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Jean-Philippe Zolesio <holblin@gmail.com>"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/adobe/css-tools.git"
},
"keywords": [
"css",
"parser",
"stringifier",
"stylesheet",
"ast",
"typescript",
"css-parser",
"css-ast",
"css-tools",
"minify",
"format",
"preprocessor"
]
}
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+218
View File
@@ -0,0 +1,218 @@
# @ampproject/remapping
> Remap sequential sourcemaps through transformations to point at the original source code
Remapping allows you to take the sourcemaps generated through transforming your code and "remap"
them to the original source locations. Think "my minified code, transformed with babel and bundled
with webpack", all pointing to the correct location in your original source code.
With remapping, none of your source code transformations need to be aware of the input's sourcemap,
they only need to generate an output sourcemap. This greatly simplifies building custom
transformations (think a find-and-replace).
## Installation
```sh
npm install @ampproject/remapping
```
## Usage
```typescript
function remapping(
map: SourceMap | SourceMap[],
loader: (file: string, ctx: LoaderContext) => (SourceMap | null | undefined),
options?: { excludeContent: boolean, decodedMappings: boolean }
): SourceMap;
// LoaderContext gives the loader the importing sourcemap, tree depth, the ability to override the
// "source" location (where child sources are resolved relative to, or the location of original
// source), and the ability to override the "content" of an original source for inclusion in the
// output sourcemap.
type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
}
```
`remapping` takes the final output sourcemap, and a `loader` function. For every source file pointer
in the sourcemap, the `loader` will be called with the resolved path. If the path itself represents
a transformed file (it has a sourcmap associated with it), then the `loader` should return that
sourcemap. If not, the path will be treated as an original, untransformed source code.
```js
// Babel transformed "helloworld.js" into "transformed.js"
const transformedMap = JSON.stringify({
file: 'transformed.js',
// 1st column of 2nd line of output file translates into the 1st source
// file, line 3, column 2
mappings: ';CAEE',
sources: ['helloworld.js'],
version: 3,
});
// Uglify minified "transformed.js" into "transformed.min.js"
const minifiedTransformedMap = JSON.stringify({
file: 'transformed.min.js',
// 0th column of 1st line of output file translates into the 1st source
// file, line 2, column 1.
mappings: 'AACC',
names: [],
sources: ['transformed.js'],
version: 3,
});
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
// The "transformed.js" file is an transformed file.
if (file === 'transformed.js') {
// The root importer is empty.
console.assert(ctx.importer === '');
// The depth in the sourcemap tree we're currently loading.
// The root `minifiedTransformedMap` is depth 0, and its source children are depth 1, etc.
console.assert(ctx.depth === 1);
return transformedMap;
}
// Loader will be called to load transformedMap's source file pointers as well.
console.assert(file === 'helloworld.js');
// `transformed.js`'s sourcemap points into `helloworld.js`.
console.assert(ctx.importer === 'transformed.js');
// This is a source child of `transformed`, which is a source child of `minifiedTransformedMap`.
console.assert(ctx.depth === 2);
return null;
}
);
console.log(remapped);
// {
// file: 'transpiled.min.js',
// mappings: 'AAEE',
// sources: ['helloworld.js'],
// version: 3,
// };
```
In this example, `loader` will be called twice:
1. `"transformed.js"`, the first source file pointer in the `minifiedTransformedMap`. We return the
associated sourcemap for it (its a transformed file, after all) so that sourcemap locations can
be traced through it into the source files it represents.
2. `"helloworld.js"`, our original, unmodified source code. This file does not have a sourcemap, so
we return `null`.
The `remapped` sourcemap now points from `transformed.min.js` into locations in `helloworld.js`. If
you were to read the `mappings`, it says "0th column of the first line output line points to the 1st
column of the 2nd line of the file `helloworld.js`".
### Multiple transformations of a file
As a convenience, if you have multiple single-source transformations of a file, you may pass an
array of sourcemap files in the order of most-recent transformation sourcemap first. Note that this
changes the `importer` and `depth` of each call to our loader. So our above example could have been
written as:
```js
const remapped = remapping(
[minifiedTransformedMap, transformedMap],
() => null
);
console.log(remapped);
// {
// file: 'transpiled.min.js',
// mappings: 'AAEE',
// sources: ['helloworld.js'],
// version: 3,
// };
```
### Advanced control of the loading graph
#### `source`
The `source` property can overridden to any value to change the location of the current load. Eg,
for an original source file, it allows us to change the location to the original source regardless
of what the sourcemap source entry says. And for transformed files, it allows us to change the
relative resolving location for child sources of the loaded sourcemap.
```js
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
if (file === 'transformed.js') {
// We pretend the transformed.js file actually exists in the 'src/' directory. When the nested
// source files are loaded, they will now be relative to `src/`.
ctx.source = 'src/transformed.js';
return transformedMap;
}
console.assert(file === 'src/helloworld.js');
// We could futher change the source of this original file, eg, to be inside a nested directory
// itself. This will be reflected in the remapped sourcemap.
ctx.source = 'src/nested/transformed.js';
return null;
}
);
console.log(remapped);
// {
// …,
// sources: ['src/nested/helloworld.js'],
// };
```
#### `content`
The `content` property can be overridden when we encounter an original source file. Eg, this allows
you to manually provide the source content of the original file regardless of whether the
`sourcesContent` field is present in the parent sourcemap. It can also be set to `null` to remove
the source content.
```js
const remapped = remapping(
minifiedTransformedMap,
(file, ctx) => {
if (file === 'transformed.js') {
// transformedMap does not include a `sourcesContent` field, so usually the remapped sourcemap
// would not include any `sourcesContent` values.
return transformedMap;
}
console.assert(file === 'helloworld.js');
// We can read the file to provide the source content.
ctx.content = fs.readFileSync(file, 'utf8');
return null;
}
);
console.log(remapped);
// {
// …,
// sourcesContent: [
// 'console.log("Hello world!")',
// ],
// };
```
### Options
#### excludeContent
By default, `excludeContent` is `false`. Passing `{ excludeContent: true }` will exclude the
`sourcesContent` field from the returned sourcemap. This is mainly useful when you want to reduce
the size out the sourcemap.
#### decodedMappings
By default, `decodedMappings` is `false`. Passing `{ decodedMappings: true }` will leave the
`mappings` field in a [decoded state](https://github.com/rich-harris/sourcemap-codec) instead of
encoding into a VLQ string.
+197
View File
@@ -0,0 +1,197 @@
import { decodedMappings, traceSegment, TraceMap } from '@jridgewell/trace-mapping';
import { GenMapping, maybeAddSegment, setSourceContent, setIgnore, toDecodedMap, toEncodedMap } from '@jridgewell/gen-mapping';
const SOURCELESS_MAPPING = /* #__PURE__ */ SegmentObject('', -1, -1, '', null, false);
const EMPTY_SOURCES = [];
function SegmentObject(source, line, column, name, content, ignore) {
return { source, line, column, name, content, ignore };
}
function Source(map, sources, source, content, ignore) {
return {
map,
sources,
source,
content,
ignore,
};
}
/**
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
* (which may themselves be SourceMapTrees).
*/
function MapSource(map, sources) {
return Source(map, sources, '', null, false);
}
/**
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
* segment tracing ends at the `OriginalSource`.
*/
function OriginalSource(source, content, ignore) {
return Source(null, EMPTY_SOURCES, source, content, ignore);
}
/**
* traceMappings is only called on the root level SourceMapTree, and begins the process of
* resolving each mapping in terms of the original source files.
*/
function traceMappings(tree) {
// TODO: Eventually support sourceRoot, which has to be removed because the sources are already
// fully resolved. We'll need to make sources relative to the sourceRoot before adding them.
const gen = new GenMapping({ file: tree.map.file });
const { sources: rootSources, map } = tree;
const rootNames = map.names;
const rootMappings = decodedMappings(map);
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
const genCol = segment[0];
let traced = SOURCELESS_MAPPING;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length !== 1) {
const source = rootSources[segment[1]];
traced = originalPositionFor(source, segment[2], segment[3], segment.length === 5 ? rootNames[segment[4]] : '');
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
// respective segment into an original source.
if (traced == null)
continue;
}
const { column, line, name, content, source, ignore } = traced;
maybeAddSegment(gen, i, genCol, source, line, column, name);
if (source && content != null)
setSourceContent(gen, source, content);
if (ignore)
setIgnore(gen, source, true);
}
}
return gen;
}
/**
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
* child SourceMapTrees, until we find the original source map.
*/
function originalPositionFor(source, line, column, name) {
if (!source.map) {
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
}
const segment = traceSegment(source.map, line, column);
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
if (segment == null)
return null;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length === 1)
return SOURCELESS_MAPPING;
return originalPositionFor(source.sources[segment[1]], segment[2], segment[3], segment.length === 5 ? source.map.names[segment[4]] : name);
}
function asArray(value) {
if (Array.isArray(value))
return value;
return [value];
}
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
function buildSourceMapTree(input, loader) {
const maps = asArray(input).map((m) => new TraceMap(m, ''));
const map = maps.pop();
for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length > 1) {
throw new Error(`Transformation map ${i} must have exactly one source file.\n` +
'Did you specify these with the most recent transformation maps first?');
}
}
let tree = build(map, loader, '', 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = MapSource(maps[i], [tree]);
}
return tree;
}
function build(map, loader, importer, importerDepth) {
const { resolvedSources, sourcesContent, ignoreList } = map;
const depth = importerDepth + 1;
const children = resolvedSources.map((sourceFile, i) => {
// The loading context gives the loader more information about why this file is being loaded
// (eg, from which importer). It also allows the loader to override the location of the loaded
// sourcemap/original source, or to override the content in the sourcesContent field if it's
// an unmodified source file.
const ctx = {
importer,
depth,
source: sourceFile || '',
content: undefined,
ignore: undefined,
};
// Use the provided loader callback to retrieve the file's sourcemap.
// TODO: We should eventually support async loading of sourcemap files.
const sourceMap = loader(ctx.source, ctx);
const { source, content, ignore } = ctx;
// If there is a sourcemap, then we need to recurse into it to load its source files.
if (sourceMap)
return build(new TraceMap(sourceMap, source), loader, source, depth);
// Else, it's an unmodified source file.
// The contents of this unmodified source file can be overridden via the loader context,
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
// the importing sourcemap's `sourcesContent` field.
const sourceContent = content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
const ignored = ignore !== undefined ? ignore : ignoreList ? ignoreList.includes(i) : false;
return OriginalSource(source, sourceContent, ignored);
});
return MapSource(map, children);
}
/**
* A SourceMap v3 compatible sourcemap, which only includes fields that were
* provided to it.
*/
class SourceMap {
constructor(map, options) {
const out = options.decodedMappings ? toDecodedMap(map) : toEncodedMap(map);
this.version = out.version; // SourceMap spec says this should be first.
this.file = out.file;
this.mappings = out.mappings;
this.names = out.names;
this.ignoreList = out.ignoreList;
this.sourceRoot = out.sourceRoot;
this.sources = out.sources;
if (!options.excludeContent) {
this.sourcesContent = out.sourcesContent;
}
}
toString() {
return JSON.stringify(this);
}
}
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
function remapping(input, loader, options) {
const opts = typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
const tree = buildSourceMapTree(input, loader);
return new SourceMap(traceMappings(tree), opts);
}
export { remapping as default };
//# sourceMappingURL=remapping.mjs.map
File diff suppressed because one or more lines are too long
+202
View File
@@ -0,0 +1,202 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@jridgewell/trace-mapping'), require('@jridgewell/gen-mapping')) :
typeof define === 'function' && define.amd ? define(['@jridgewell/trace-mapping', '@jridgewell/gen-mapping'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.remapping = factory(global.traceMapping, global.genMapping));
})(this, (function (traceMapping, genMapping) { 'use strict';
const SOURCELESS_MAPPING = /* #__PURE__ */ SegmentObject('', -1, -1, '', null, false);
const EMPTY_SOURCES = [];
function SegmentObject(source, line, column, name, content, ignore) {
return { source, line, column, name, content, ignore };
}
function Source(map, sources, source, content, ignore) {
return {
map,
sources,
source,
content,
ignore,
};
}
/**
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
* (which may themselves be SourceMapTrees).
*/
function MapSource(map, sources) {
return Source(map, sources, '', null, false);
}
/**
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
* segment tracing ends at the `OriginalSource`.
*/
function OriginalSource(source, content, ignore) {
return Source(null, EMPTY_SOURCES, source, content, ignore);
}
/**
* traceMappings is only called on the root level SourceMapTree, and begins the process of
* resolving each mapping in terms of the original source files.
*/
function traceMappings(tree) {
// TODO: Eventually support sourceRoot, which has to be removed because the sources are already
// fully resolved. We'll need to make sources relative to the sourceRoot before adding them.
const gen = new genMapping.GenMapping({ file: tree.map.file });
const { sources: rootSources, map } = tree;
const rootNames = map.names;
const rootMappings = traceMapping.decodedMappings(map);
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
for (let j = 0; j < segments.length; j++) {
const segment = segments[j];
const genCol = segment[0];
let traced = SOURCELESS_MAPPING;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length !== 1) {
const source = rootSources[segment[1]];
traced = originalPositionFor(source, segment[2], segment[3], segment.length === 5 ? rootNames[segment[4]] : '');
// If the trace is invalid, then the trace ran into a sourcemap that doesn't contain a
// respective segment into an original source.
if (traced == null)
continue;
}
const { column, line, name, content, source, ignore } = traced;
genMapping.maybeAddSegment(gen, i, genCol, source, line, column, name);
if (source && content != null)
genMapping.setSourceContent(gen, source, content);
if (ignore)
genMapping.setIgnore(gen, source, true);
}
}
return gen;
}
/**
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
* child SourceMapTrees, until we find the original source map.
*/
function originalPositionFor(source, line, column, name) {
if (!source.map) {
return SegmentObject(source.source, line, column, name, source.content, source.ignore);
}
const segment = traceMapping.traceSegment(source.map, line, column);
// If we couldn't find a segment, then this doesn't exist in the sourcemap.
if (segment == null)
return null;
// 1-length segments only move the current generated column, there's no source information
// to gather from it.
if (segment.length === 1)
return SOURCELESS_MAPPING;
return originalPositionFor(source.sources[segment[1]], segment[2], segment[3], segment.length === 5 ? source.map.names[segment[4]] : name);
}
function asArray(value) {
if (Array.isArray(value))
return value;
return [value];
}
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
function buildSourceMapTree(input, loader) {
const maps = asArray(input).map((m) => new traceMapping.TraceMap(m, ''));
const map = maps.pop();
for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length > 1) {
throw new Error(`Transformation map ${i} must have exactly one source file.\n` +
'Did you specify these with the most recent transformation maps first?');
}
}
let tree = build(map, loader, '', 0);
for (let i = maps.length - 1; i >= 0; i--) {
tree = MapSource(maps[i], [tree]);
}
return tree;
}
function build(map, loader, importer, importerDepth) {
const { resolvedSources, sourcesContent, ignoreList } = map;
const depth = importerDepth + 1;
const children = resolvedSources.map((sourceFile, i) => {
// The loading context gives the loader more information about why this file is being loaded
// (eg, from which importer). It also allows the loader to override the location of the loaded
// sourcemap/original source, or to override the content in the sourcesContent field if it's
// an unmodified source file.
const ctx = {
importer,
depth,
source: sourceFile || '',
content: undefined,
ignore: undefined,
};
// Use the provided loader callback to retrieve the file's sourcemap.
// TODO: We should eventually support async loading of sourcemap files.
const sourceMap = loader(ctx.source, ctx);
const { source, content, ignore } = ctx;
// If there is a sourcemap, then we need to recurse into it to load its source files.
if (sourceMap)
return build(new traceMapping.TraceMap(sourceMap, source), loader, source, depth);
// Else, it's an unmodified source file.
// The contents of this unmodified source file can be overridden via the loader context,
// allowing it to be explicitly null or a string. If it remains undefined, we fall back to
// the importing sourcemap's `sourcesContent` field.
const sourceContent = content !== undefined ? content : sourcesContent ? sourcesContent[i] : null;
const ignored = ignore !== undefined ? ignore : ignoreList ? ignoreList.includes(i) : false;
return OriginalSource(source, sourceContent, ignored);
});
return MapSource(map, children);
}
/**
* A SourceMap v3 compatible sourcemap, which only includes fields that were
* provided to it.
*/
class SourceMap {
constructor(map, options) {
const out = options.decodedMappings ? genMapping.toDecodedMap(map) : genMapping.toEncodedMap(map);
this.version = out.version; // SourceMap spec says this should be first.
this.file = out.file;
this.mappings = out.mappings;
this.names = out.names;
this.ignoreList = out.ignoreList;
this.sourceRoot = out.sourceRoot;
this.sources = out.sources;
if (!options.excludeContent) {
this.sourcesContent = out.sourcesContent;
}
}
toString() {
return JSON.stringify(this);
}
}
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
function remapping(input, loader, options) {
const opts = typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
const tree = buildSourceMapTree(input, loader);
return new SourceMap(traceMappings(tree), opts);
}
return remapping;
}));
//# sourceMappingURL=remapping.umd.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
import type { MapSource as MapSourceType } from './source-map-tree';
import type { SourceMapInput, SourceMapLoader } from './types';
/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
* `OriginalSource`s and `SourceMapTree`s.
*
* Every sourcemap is composed of a collection of source files and mappings
* into locations of those source files. When we generate a `SourceMapTree` for
* the sourcemap, we attempt to load each source file's own sourcemap. If it
* does not have an associated sourcemap, it is considered an original,
* unmodified source file.
*/
export default function buildSourceMapTree(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader): MapSourceType;
+20
View File
@@ -0,0 +1,20 @@
import SourceMap from './source-map';
import type { SourceMapInput, SourceMapLoader, Options } from './types';
export type { SourceMapSegment, EncodedSourceMap, EncodedSourceMap as RawSourceMap, DecodedSourceMap, SourceMapInput, SourceMapLoader, LoaderContext, Options, } from './types';
export type { SourceMap };
/**
* Traces through all the mappings in the root sourcemap, through the sources
* (and their sourcemaps), all the way back to the original source location.
*
* `loader` will be called every time we encounter a source file. If it returns
* a sourcemap, we will recurse into that sourcemap to continue the trace. If
* it returns a falsey value, that source file is treated as an original,
* unmodified source file.
*
* Pass `excludeContent` to exclude any self-containing source file content
* from the output sourcemap.
*
* Pass `decodedMappings` to receive a SourceMap with decoded (instead of
* VLQ encoded) mappings.
*/
export default function remapping(input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader, options?: boolean | Options): SourceMap;
@@ -0,0 +1,45 @@
import { GenMapping } from '@jridgewell/gen-mapping';
import type { TraceMap } from '@jridgewell/trace-mapping';
export declare type SourceMapSegmentObject = {
column: number;
line: number;
name: string;
source: string;
content: string | null;
ignore: boolean;
};
export declare type OriginalSource = {
map: null;
sources: Sources[];
source: string;
content: string | null;
ignore: boolean;
};
export declare type MapSource = {
map: TraceMap;
sources: Sources[];
source: string;
content: null;
ignore: false;
};
export declare type Sources = OriginalSource | MapSource;
/**
* MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes
* (which may themselves be SourceMapTrees).
*/
export declare function MapSource(map: TraceMap, sources: Sources[]): MapSource;
/**
* A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive
* segment tracing ends at the `OriginalSource`.
*/
export declare function OriginalSource(source: string, content: string | null, ignore: boolean): OriginalSource;
/**
* traceMappings is only called on the root level SourceMapTree, and begins the process of
* resolving each mapping in terms of the original source files.
*/
export declare function traceMappings(tree: MapSource): GenMapping;
/**
* originalPositionFor is only called on children SourceMapTrees. It recurses down into its own
* child SourceMapTrees, until we find the original source map.
*/
export declare function originalPositionFor(source: Sources, line: number, column: number, name: string): SourceMapSegmentObject | null;
@@ -0,0 +1,18 @@
import type { GenMapping } from '@jridgewell/gen-mapping';
import type { DecodedSourceMap, EncodedSourceMap, Options } from './types';
/**
* A SourceMap v3 compatible sourcemap, which only includes fields that were
* provided to it.
*/
export default class SourceMap {
file?: string | null;
mappings: EncodedSourceMap['mappings'] | DecodedSourceMap['mappings'];
sourceRoot?: string;
names: string[];
sources: (string | null)[];
sourcesContent?: (string | null)[];
version: 3;
ignoreList: number[] | undefined;
constructor(map: GenMapping, options: Options);
toString(): string;
}
+15
View File
@@ -0,0 +1,15 @@
import type { SourceMapInput } from '@jridgewell/trace-mapping';
export type { SourceMapSegment, DecodedSourceMap, EncodedSourceMap, } from '@jridgewell/trace-mapping';
export type { SourceMapInput };
export declare type LoaderContext = {
readonly importer: string;
readonly depth: number;
source: string;
content: string | null | undefined;
ignore: boolean | undefined;
};
export declare type SourceMapLoader = (file: string, ctx: LoaderContext) => SourceMapInput | null | undefined | void;
export declare type Options = {
excludeContent?: boolean;
decodedMappings?: boolean;
};
+75
View File
@@ -0,0 +1,75 @@
{
"name": "@ampproject/remapping",
"version": "2.3.0",
"description": "Remap sequential sourcemaps through transformations to point at the original source code",
"keywords": [
"source",
"map",
"remap"
],
"main": "dist/remapping.umd.js",
"module": "dist/remapping.mjs",
"types": "dist/types/remapping.d.ts",
"exports": {
".": [
{
"types": "./dist/types/remapping.d.ts",
"browser": "./dist/remapping.umd.js",
"require": "./dist/remapping.umd.js",
"import": "./dist/remapping.mjs"
},
"./dist/remapping.umd.js"
],
"./package.json": "./package.json"
},
"files": [
"dist"
],
"author": "Justin Ridgewell <jridgewell@google.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/ampproject/remapping.git"
},
"license": "Apache-2.0",
"engines": {
"node": ">=6.0.0"
},
"scripts": {
"build": "run-s -n build:*",
"build:rollup": "rollup -c rollup.config.js",
"build:ts": "tsc --project tsconfig.build.json",
"lint": "run-s -n lint:*",
"lint:prettier": "npm run test:lint:prettier -- --write",
"lint:ts": "npm run test:lint:ts -- --fix",
"prebuild": "rm -rf dist",
"prepublishOnly": "npm run preversion",
"preversion": "run-s test build",
"test": "run-s -n test:lint test:only",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"test:lint": "run-s -n test:lint:*",
"test:lint:prettier": "prettier --check '{src,test}/**/*.ts'",
"test:lint:ts": "eslint '{src,test}/**/*.ts'",
"test:only": "jest --coverage",
"test:watch": "jest --coverage --watch"
},
"devDependencies": {
"@rollup/plugin-typescript": "8.3.2",
"@types/jest": "27.4.1",
"@typescript-eslint/eslint-plugin": "5.20.0",
"@typescript-eslint/parser": "5.20.0",
"eslint": "8.14.0",
"eslint-config-prettier": "8.5.0",
"jest": "27.5.1",
"jest-config": "27.5.1",
"npm-run-all": "4.1.5",
"prettier": "2.6.2",
"rollup": "2.70.2",
"ts-jest": "27.1.4",
"tslib": "2.4.0",
"typescript": "4.6.3"
},
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
# Angular
The sources for this package are in the main [Angular](https://github.com/angular/angular) repo. Please file issues and pull requests against that repo.
Usage information and reference details can be found in [Angular documentation](https://angular.dev/overview).
License: MIT
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More