2023-09-11 18:04:44 +00:00
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types/experimental' ;
2023-06-30 09:09:21 +00:00
import type { AstroAdapter , AstroConfig , AstroIntegration , RouteData } from 'astro' ;
2023-09-11 18:04:44 +00:00
import { createRedirectsFromAstroRoutes } from '@astrojs/underscore-redirects' ;
import { CacheStorage } from '@miniflare/cache' ;
import { NoOpLog } from '@miniflare/shared' ;
import { MemoryStorage } from '@miniflare/storage-memory' ;
import { AstroError } from 'astro/errors' ;
2022-06-16 14:12:25 +00:00
import esbuild from 'esbuild' ;
2023-07-18 00:17:59 +00:00
import * as fs from 'node:fs' ;
import * as os from 'node:os' ;
import { sep } from 'node:path' ;
import { fileURLToPath , pathToFileURL } from 'node:url' ;
2022-11-21 13:31:21 +00:00
import glob from 'tiny-glob' ;
2023-09-11 18:04:44 +00:00
import { getEnvVars } from './parser.js' ;
2022-06-16 14:12:25 +00:00
2023-09-13 14:49:22 +00:00
export type { AdvancedRuntime } from './server.advanced.js' ;
export type { DirectoryRuntime } from './server.directory.js' ;
2023-08-23 15:00:56 +00:00
2022-08-08 17:10:48 +00:00
type Options = {
2023-09-11 18:04:44 +00:00
mode ? : 'directory' | 'advanced' ;
2023-08-17 21:40:28 +00:00
functionPerRoute? : boolean ;
2023-09-11 18:04:44 +00:00
/ * *
* 'off' : current behaviour ( wrangler is needed )
* 'local' : use a static req . cf object , and env vars defined in wrangler . toml & . dev . vars ( astro dev is enough )
* 'remote' : use a dynamic real - live req . cf object , and env vars defined in wrangler . toml & . dev . vars ( astro dev is enough )
* /
runtime ? : 'off' | 'local' | 'remote' ;
2022-08-08 17:10:48 +00:00
} ;
2022-10-12 21:25:51 +00:00
interface BuildConfig {
server : URL ;
client : URL ;
serverEntry : string ;
2023-06-30 09:09:21 +00:00
split? : boolean ;
2022-10-12 21:25:51 +00:00
}
2023-09-11 18:04:44 +00:00
class StorageFactory {
storages = new Map ( ) ;
storage ( namespace : string ) {
let storage = this . storages . get ( namespace ) ;
if ( storage ) return storage ;
this . storages . set ( namespace , ( storage = new MemoryStorage ( ) ) ) ;
return storage ;
}
}
2023-08-17 21:40:28 +00:00
export function getAdapter ( {
isModeDirectory ,
functionPerRoute ,
} : {
isModeDirectory : boolean ;
functionPerRoute : boolean ;
} ) : AstroAdapter {
2022-08-08 17:10:48 +00:00
return isModeDirectory
? {
2023-07-17 13:15:32 +00:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.directory.js' ,
exports : [ 'onRequest' , 'manifest' ] ,
2023-08-17 21:40:28 +00:00
adapterFeatures : {
functionPerRoute ,
edgeMiddleware : false ,
} ,
2023-07-28 11:15:09 +00:00
supportedAstroFeatures : {
hybridOutput : 'stable' ,
staticOutput : 'unsupported' ,
serverOutput : 'stable' ,
assets : {
2023-08-14 19:23:36 +00:00
supportKind : 'stable' ,
2023-07-28 11:15:09 +00:00
isSharpCompatible : false ,
isSquooshCompatible : false ,
} ,
} ,
2023-07-17 13:15:32 +00:00
}
2022-08-08 17:10:48 +00:00
: {
2023-07-17 13:15:32 +00:00
name : '@astrojs/cloudflare' ,
serverEntrypoint : '@astrojs/cloudflare/server.advanced.js' ,
exports : [ 'default' ] ,
2023-07-28 11:15:09 +00:00
supportedAstroFeatures : {
hybridOutput : 'stable' ,
staticOutput : 'unsupported' ,
serverOutput : 'stable' ,
assets : {
supportKind : 'stable' ,
isSharpCompatible : false ,
isSquooshCompatible : false ,
} ,
} ,
2023-07-17 13:15:32 +00:00
} ;
2022-06-16 14:12:25 +00:00
}
2023-09-11 18:04:44 +00:00
async function getCFObject ( runtimeMode : string ) : Promise < IncomingRequestCfProperties | void > {
const CF_ENDPOINT = 'https://workers.cloudflare.com/cf.json' ;
const CF_FALLBACK : IncomingRequestCfProperties = {
asOrganization : '' ,
asn : 395747 ,
colo : 'DFW' ,
city : 'Austin' ,
region : 'Texas' ,
regionCode : 'TX' ,
metroCode : '635' ,
postalCode : '78701' ,
country : 'US' ,
continent : 'NA' ,
timezone : 'America/Chicago' ,
latitude : '30.27130' ,
longitude : '-97.74260' ,
clientTcpRtt : 0 ,
httpProtocol : 'HTTP/1.1' ,
requestPriority : 'weight=192;exclusive=0' ,
tlsCipher : 'AEAD-AES128-GCM-SHA256' ,
tlsVersion : 'TLSv1.3' ,
tlsClientAuth : {
certPresented : '0' ,
certVerified : 'NONE' ,
certRevoked : '0' ,
certIssuerDN : '' ,
certSubjectDN : '' ,
certIssuerDNRFC2253 : '' ,
certSubjectDNRFC2253 : '' ,
certIssuerDNLegacy : '' ,
certSubjectDNLegacy : '' ,
certSerial : '' ,
certIssuerSerial : '' ,
certSKI : '' ,
certIssuerSKI : '' ,
certFingerprintSHA1 : '' ,
certFingerprintSHA256 : '' ,
certNotBefore : '' ,
certNotAfter : '' ,
} ,
edgeRequestKeepAliveStatus : 0 ,
hostMetadata : undefined ,
clientTrustScore : 99 ,
botManagement : {
corporateProxy : false ,
verifiedBot : false ,
ja3Hash : '25b4882c2bcb50cd6b469ff28c596742' ,
staticResource : false ,
detectionIds : [ ] ,
score : 99 ,
} ,
} ;
if ( runtimeMode === 'local' ) {
return CF_FALLBACK ;
} else if ( runtimeMode === 'remote' ) {
try {
const res = await fetch ( CF_ENDPOINT ) ;
const cfText = await res . text ( ) ;
const storedCf = JSON . parse ( cfText ) ;
return storedCf ;
} catch ( e : any ) {
return CF_FALLBACK ;
}
}
}
2022-07-27 20:15:19 +00:00
const SHIM = ` globalThis.process = {
argv : [ ] ,
env : { } ,
} ; ` ;
2022-11-21 13:31:21 +00:00
const SERVER_BUILD_FOLDER = '/$server_build/' ;
2023-08-10 03:04:09 +00:00
/ * *
* These route types are candiates for being part of the ` _routes.json ` ` include ` array .
* /
const potentialFunctionRouteTypes = [ 'endpoint' , 'page' ] ;
2022-08-08 17:10:48 +00:00
export default function createIntegration ( args? : Options ) : AstroIntegration {
2022-06-16 14:12:25 +00:00
let _config : AstroConfig ;
let _buildConfig : BuildConfig ;
2023-06-30 09:09:21 +00:00
let _entryPoints = new Map < RouteData , URL > ( ) ;
2022-06-16 14:12:25 +00:00
2023-08-17 21:40:28 +00:00
const isModeDirectory = args ? . mode === 'directory' ;
const functionPerRoute = args ? . functionPerRoute ? ? false ;
2023-09-11 18:04:44 +00:00
const runtimeMode = args ? . runtime ? ? 'off' ;
2023-08-17 21:40:28 +00:00
2022-06-16 14:12:25 +00:00
return {
name : '@astrojs/cloudflare' ,
hooks : {
2022-10-12 21:25:51 +00:00
'astro:config:setup' : ( { config , updateConfig } ) = > {
updateConfig ( {
build : {
2022-11-21 13:31:21 +00:00
client : new URL ( ` . ${ config . base } ` , config . outDir ) ,
server : new URL ( ` . ${ SERVER_BUILD_FOLDER } ` , config . outDir ) ,
2023-01-26 17:43:39 +00:00
serverEntry : '_worker.mjs' ,
2023-06-05 13:03:20 +00:00
redirects : false ,
2022-10-12 21:27:56 +00:00
} ,
2022-10-12 21:25:51 +00:00
} ) ;
} ,
2022-06-16 14:12:25 +00:00
'astro:config:done' : ( { setAdapter , config } ) = > {
2023-08-17 21:40:28 +00:00
setAdapter ( getAdapter ( { isModeDirectory , functionPerRoute } ) ) ;
2022-06-16 14:12:25 +00:00
_config = config ;
2022-10-12 21:25:51 +00:00
_buildConfig = config . build ;
2022-07-25 04:18:02 +00:00
2022-07-25 04:20:38 +00:00
if ( config . output === 'static' ) {
2023-09-11 18:04:44 +00:00
throw new AstroError (
'[@astrojs/cloudflare] `output: "server"` or `output: "hybrid"` is required to use this adapter. Otherwise, this adapter is not necessary to deploy a static site to Cloudflare.'
) ;
2022-07-25 04:18:02 +00:00
}
2022-11-21 13:31:21 +00:00
if ( config . base === SERVER_BUILD_FOLDER ) {
2023-09-11 18:04:44 +00:00
throw new AstroError (
'[@astrojs/cloudflare] `base: "${SERVER_BUILD_FOLDER}"` is not allowed. Please change your `base` config to something else.'
) ;
}
} ,
'astro:server:setup' : ( { server } ) = > {
if ( runtimeMode !== 'off' ) {
server . middlewares . use ( async function middleware ( req , res , next ) {
try {
const cf = await getCFObject ( runtimeMode ) ;
const vars = await getEnvVars ( ) ;
const clientLocalsSymbol = Symbol . for ( 'astro.locals' ) ;
Reflect . set ( req , clientLocalsSymbol , {
runtime : {
env : {
// default binding for static assets will be dynamic once we support mocking of bindings
ASSETS : { } ,
// this is just a VAR for CF to change build behavior, on dev it should be 0
CF_PAGES : '0' ,
// will be fetched from git dynamically once we support mocking of bindings
CF_PAGES_BRANCH : 'TBA' ,
// will be fetched from git dynamically once we support mocking of bindings
CF_PAGES_COMMIT_SHA : 'TBA' ,
CF_PAGES_URL : ` http:// ${ req . headers . host } ` ,
. . . vars ,
} ,
cf : cf ,
waitUntil : ( _promise : Promise < any > ) = > {
return ;
} ,
caches : new CacheStorage (
{ cache : true , cachePersist : false } ,
new NoOpLog ( ) ,
new StorageFactory ( ) ,
{ }
) ,
} ,
} ) ;
next ( ) ;
} catch {
next ( ) ;
}
} ) ;
2022-11-21 13:31:21 +00:00
}
2022-06-16 14:12:25 +00:00
} ,
'astro:build:setup' : ( { vite , target } ) = > {
if ( target === 'server' ) {
2023-05-17 07:44:20 +00:00
vite . resolve || = { } ;
vite . resolve . alias || = { } ;
2022-06-16 14:12:25 +00:00
const aliases = [ { find : 'react-dom/server' , replacement : 'react-dom/server.browser' } ] ;
if ( Array . isArray ( vite . resolve . alias ) ) {
vite . resolve . alias = [ . . . vite . resolve . alias , . . . aliases ] ;
} else {
for ( const alias of aliases ) {
( vite . resolve . alias as Record < string , string > ) [ alias . find ] = alias . replacement ;
}
}
2023-05-17 07:44:20 +00:00
vite . ssr || = { } ;
vite . ssr . target = 'webworker' ;
2023-07-17 12:57:08 +00:00
// Cloudflare env is only available per request. This isn't feasible for code that access env vars
// in a global way, so we shim their access as `process.env.*`. We will populate `process.env` later
// in its fetch handler.
vite . define = {
'process.env' : 'process.env' ,
. . . vite . define ,
} ;
2022-06-16 14:12:25 +00:00
}
} ,
2023-07-03 12:59:43 +00:00
'astro:build:ssr' : ( { entryPoints } ) = > {
2023-06-30 09:09:21 +00:00
_entryPoints = entryPoints ;
} ,
2023-06-05 13:03:20 +00:00
'astro:build:done' : async ( { pages , routes , dir } ) = > {
2023-06-30 09:09:21 +00:00
const functionsUrl = new URL ( 'functions/' , _config . root ) ;
if ( isModeDirectory ) {
await fs . promises . mkdir ( functionsUrl , { recursive : true } ) ;
}
2023-08-17 21:40:28 +00:00
// TODO: remove _buildConfig.split in Astro 4.0
if ( isModeDirectory && ( _buildConfig . split || functionPerRoute ) ) {
2023-06-30 09:12:30 +00:00
const entryPointsURL = [ . . . _entryPoints . values ( ) ] ;
2023-06-30 09:09:21 +00:00
const entryPaths = entryPointsURL . map ( ( entry ) = > fileURLToPath ( entry ) ) ;
2023-07-17 13:15:32 +00:00
const outputUrl = new URL ( '$astro' , _buildConfig . server ) ;
2023-07-17 13:12:41 +00:00
const outputDir = fileURLToPath ( outputUrl ) ;
2023-06-30 09:09:21 +00:00
2023-07-17 13:12:41 +00:00
await esbuild . build ( {
2023-06-30 09:09:21 +00:00
target : 'es2020' ,
platform : 'browser' ,
conditions : [ 'workerd' , 'worker' , 'browser' ] ,
2023-09-19 09:51:06 +00:00
external : [
'node:assert' ,
'node:async_hooks' ,
'node:buffer' ,
'node:diagnostics_channel' ,
'node:events' ,
'node:path' ,
'node:process' ,
'node:stream' ,
'node:string_decoder' ,
'node:util' ,
] ,
2023-06-30 09:09:21 +00:00
entryPoints : entryPaths ,
outdir : outputDir ,
allowOverwrite : true ,
format : 'esm' ,
bundle : true ,
minify : _config.vite?.build?.minify !== false ,
banner : {
js : SHIM ,
} ,
logOverride : {
'ignored-bare-import' : 'silent' ,
} ,
} ) ;
2022-06-16 14:12:25 +00:00
2023-07-17 13:15:32 +00:00
const outputFiles : Array < string > = await glob ( ` **/* ` , {
cwd : outputDir ,
filesOnly : true ,
} ) ;
2023-07-17 13:12:41 +00:00
// move the files into the functions folder
// & make sure the file names match Cloudflare syntax for routing
for ( const outputFile of outputFiles ) {
const path = outputFile . split ( sep ) ;
2023-07-17 13:15:32 +00:00
const finalSegments = path . map ( ( segment ) = >
segment
. replace ( /(\_)(\w+)(\_)/g , ( _ , __ , prop ) = > {
return ` [ ${ prop } ] ` ;
} )
. replace ( /(\_\-\-\-)(\w+)(\_)/g , ( _ , __ , prop ) = > {
return ` [[ ${ prop } ]] ` ;
} )
2023-07-17 13:12:41 +00:00
) ;
finalSegments [ finalSegments . length - 1 ] = finalSegments [ finalSegments . length - 1 ]
. replace ( 'entry.' , '' )
. replace ( /(.*)\.(\w+)\.(\w+)$/g , ( _ , fileName , __ , newExt ) = > {
return ` ${ fileName } . ${ newExt } ` ;
2023-07-17 13:15:32 +00:00
} ) ;
2023-07-17 13:12:41 +00:00
const finalDirPath = finalSegments . slice ( 0 , - 1 ) . join ( sep ) ;
const finalPath = finalSegments . join ( sep ) ;
const newDirUrl = new URL ( finalDirPath , functionsUrl ) ;
2023-07-17 13:15:32 +00:00
await fs . promises . mkdir ( newDirUrl , { recursive : true } ) ;
2023-07-17 13:12:41 +00:00
const oldFileUrl = new URL ( ` $ astro/ ${ outputFile } ` , outputUrl ) ;
const newFileUrl = new URL ( finalPath , functionsUrl ) ;
await fs . promises . rename ( oldFileUrl , newFileUrl ) ;
2023-06-30 09:09:21 +00:00
}
} else {
const entryPath = fileURLToPath ( new URL ( _buildConfig . serverEntry , _buildConfig . server ) ) ;
const entryUrl = new URL ( _buildConfig . serverEntry , _config . outDir ) ;
const buildPath = fileURLToPath ( entryUrl ) ;
// A URL for the final build path after renaming
const finalBuildUrl = pathToFileURL ( buildPath . replace ( /\.mjs$/ , '.js' ) ) ;
await esbuild . build ( {
target : 'es2020' ,
platform : 'browser' ,
conditions : [ 'workerd' , 'worker' , 'browser' ] ,
2023-09-19 09:51:06 +00:00
external : [
'node:assert' ,
'node:async_hooks' ,
'node:buffer' ,
'node:diagnostics_channel' ,
'node:events' ,
'node:path' ,
'node:process' ,
'node:stream' ,
'node:string_decoder' ,
'node:util' ,
] ,
2023-06-30 09:09:21 +00:00
entryPoints : [ entryPath ] ,
outfile : buildPath ,
allowOverwrite : true ,
format : 'esm' ,
bundle : true ,
minify : _config.vite?.build?.minify !== false ,
banner : {
js : SHIM ,
} ,
logOverride : {
'ignored-bare-import' : 'silent' ,
} ,
} ) ;
// Rename to worker.js
await fs . promises . rename ( buildPath , finalBuildUrl ) ;
if ( isModeDirectory ) {
const directoryUrl = new URL ( '[[path]].js' , functionsUrl ) ;
await fs . promises . rename ( finalBuildUrl , directoryUrl ) ;
}
}
2023-08-10 15:19:00 +00:00
// throw the server folder in the bin
2022-11-21 13:31:21 +00:00
const serverUrl = new URL ( _buildConfig . server ) ;
await fs . promises . rm ( serverUrl , { recursive : true , force : true } ) ;
// move cloudflare specific files to the root
const cloudflareSpecialFiles = [ '_headers' , '_redirects' , '_routes.json' ] ;
if ( _config . base !== '/' ) {
for ( const file of cloudflareSpecialFiles ) {
try {
await fs . promises . rename (
new URL ( file , _buildConfig . client ) ,
new URL ( file , _config . outDir )
) ;
} catch ( e ) {
// ignore
}
}
}
const routesExists = await fs . promises
. stat ( new URL ( './_routes.json' , _config . outDir ) )
. then ( ( stat ) = > stat . isFile ( ) )
. catch ( ( ) = > false ) ;
// this creates a _routes.json, in case there is none present to enable
// cloudflare to handle static files and support _redirects configuration
// (without calling the function)
if ( ! routesExists ) {
2023-08-10 03:04:09 +00:00
const functionEndpoints = routes
// Certain route types, when their prerender option is set to false, a run on the server as function invocations
. filter ( ( route ) = > potentialFunctionRouteTypes . includes ( route . type ) && ! route . prerender )
. map ( ( route ) = > {
const includePattern =
'/' +
route . segments
. flat ( )
. map ( ( segment ) = > ( segment . dynamic ? '*' : segment . content ) )
. join ( '/' ) ;
const regexp = new RegExp (
'^\\/' +
route . segments
. flat ( )
. map ( ( segment ) = > ( segment . dynamic ? '(.*)' : segment . content ) )
. join ( '\\/' ) +
'$'
) ;
return {
includePattern ,
regexp ,
} ;
} ) ;
2022-11-21 13:31:21 +00:00
const staticPathList : Array < string > = (
await glob ( ` ${ fileURLToPath ( _buildConfig . client ) } /**/* ` , {
cwd : fileURLToPath ( _config . outDir ) ,
filesOnly : true ,
} )
)
. filter ( ( file : string ) = > cloudflareSpecialFiles . indexOf ( file ) < 0 )
2023-08-10 03:04:09 +00:00
. map ( ( file : string ) = > ` / ${ file . replace ( /\\/g , '/' ) } ` ) ;
2022-11-21 13:31:21 +00:00
2023-01-26 17:45:39 +00:00
for ( let page of pages ) {
2023-03-13 14:58:21 +00:00
let pagePath = prependForwardSlash ( page . pathname ) ;
if ( _config . base !== '/' ) {
2023-03-16 14:42:36 +00:00
const base = _config . base . endsWith ( '/' ) ? _config . base . slice ( 0 , - 1 ) : _config . base ;
2023-03-13 14:58:21 +00:00
pagePath = ` ${ base } ${ pagePath } ` ;
}
staticPathList . push ( pagePath ) ;
2023-01-26 17:43:39 +00:00
}
2022-11-21 13:31:21 +00:00
const redirectsExists = await fs . promises
. stat ( new URL ( './_redirects' , _config . outDir ) )
. then ( ( stat ) = > stat . isFile ( ) )
. catch ( ( ) = > false ) ;
2022-11-21 13:33:45 +00:00
// convert all redirect source paths into a list of routes
// and add them to the static path
2022-11-21 13:31:21 +00:00
if ( redirectsExists ) {
const redirects = (
await fs . promises . readFile ( new URL ( './_redirects' , _config . outDir ) , 'utf-8' )
)
. split ( os . EOL )
. map ( ( line ) = > {
const parts = line . split ( ' ' ) ;
if ( parts . length < 2 ) {
return null ;
} else {
// convert /products/:id to /products/*
return (
parts [ 0 ]
. replace ( /\/:.*?(?=\/|$)/g , '/*' )
// remove query params as they are not supported by cloudflare
. replace ( /\?.*$/ , '' )
) ;
}
} )
. filter (
( line , index , arr ) = > line !== null && arr . indexOf ( line ) === index
) as string [ ] ;
if ( redirects . length > 0 ) {
staticPathList . push ( . . . redirects ) ;
}
}
2023-07-13 08:21:33 +00:00
const redirectRoutes : [ RouteData , string ] [ ] = routes
. filter ( ( r ) = > r . type === 'redirect' )
. map ( ( r ) = > {
return [ r , '' ] ;
} ) ;
2023-06-05 13:03:20 +00:00
const trueRedirects = createRedirectsFromAstroRoutes ( {
config : _config ,
2023-07-13 08:21:33 +00:00
routeToDynamicTargetMap : new Map ( Array . from ( redirectRoutes ) ) ,
2023-06-05 13:03:20 +00:00
dir ,
} ) ;
2023-06-05 13:05:47 +00:00
if ( ! trueRedirects . empty ( ) ) {
2023-06-05 13:03:20 +00:00
await fs . promises . appendFile (
new URL ( './_redirects' , _config . outDir ) ,
trueRedirects . print ( )
) ;
}
2023-08-10 03:04:09 +00:00
staticPathList . push ( . . . routes . filter ( ( r ) = > r . type === 'redirect' ) . map ( ( r ) = > r . route ) ) ;
// In order to product the shortest list of patterns, we first try to
// include all function endpoints, and then exclude all static paths
let include = deduplicatePatterns (
functionEndpoints . map ( ( endpoint ) = > endpoint . includePattern )
) ;
let exclude = deduplicatePatterns (
staticPathList . filter ( ( file : string ) = >
functionEndpoints . some ( ( endpoint ) = > endpoint . regexp . test ( file ) )
)
) ;
// Cloudflare requires at least one include pattern:
// https://developers.cloudflare.com/pages/platform/functions/routing/#limits
// So we add a pattern that we immediately exclude again
if ( include . length === 0 ) {
include = [ '/' ] ;
exclude = [ '/' ] ;
}
// If using only an exclude list would produce a shorter list of patterns,
// we use that instead
if ( include . length + exclude . length > staticPathList . length ) {
include = [ '/*' ] ;
exclude = deduplicatePatterns ( staticPathList ) ;
}
2022-11-21 13:31:21 +00:00
await fs . promises . writeFile (
new URL ( './_routes.json' , _config . outDir ) ,
JSON . stringify (
{
version : 1 ,
2023-08-10 03:04:09 +00:00
include ,
exclude ,
2022-11-21 13:31:21 +00:00
} ,
null ,
2
)
) ;
}
2022-06-16 14:12:25 +00:00
} ,
} ,
} ;
}
2023-01-26 17:43:39 +00:00
function prependForwardSlash ( path : string ) {
return path [ 0 ] === '/' ? path : '/' + path ;
}
2023-08-10 03:04:09 +00:00
/ * *
* Remove duplicates and redundant patterns from an ` include ` or ` exclude ` list .
* Otherwise Cloudflare will throw an error on deployment . Plus , it saves more entries .
* E . g . ` ['/foo/*', '/foo/*', '/foo/bar'] => ['/foo/*'] `
* @param patterns a list of ` include ` or ` exclude ` patterns
* @returns a deduplicated list of patterns
* /
function deduplicatePatterns ( patterns : string [ ] ) {
const openPatterns : RegExp [ ] = [ ] ;
return [ . . . new Set ( patterns ) ]
. sort ( ( a , b ) = > a . length - b . length )
. filter ( ( pattern ) = > {
if ( openPatterns . some ( ( p ) = > p . test ( pattern ) ) ) {
return false ;
}
if ( pattern . endsWith ( '*' ) ) {
openPatterns . push ( new RegExp ( ` ^ ${ pattern . replace ( /(\*\/)*\*$/g , '.*' ) } ` ) ) ;
}
return true ;
} ) ;
}