Skip to content

Commit adf1c83

Browse files
committed
feat: enable path aliases if specified in paths or js/tsconfig json
- Update RunEsbuildJs to optionally include tsconfig in the command arguments.
1 parent 5371f54 commit adf1c83

File tree

6 files changed

+68
-149
lines changed

6 files changed

+68
-149
lines changed

src/Perla/Build.fs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -341,23 +341,14 @@ module BuildService =
341341
ex.Message
342342
)
343343

344-
member _.LoadPlugins(config, vfsOutputDir) =
345-
let config = config |> AVal.force
344+
member _.LoadPlugins(configA, vfsOutputDir) =
345+
let config = configA |> AVal.force
346346
let plugins = args.FsManager.ResolvePluginPaths()
347347

348348
let isEsbuildPluginPresent =
349349
config.plugins |> List.contains Constants.PerlaEsbuildPluginName
350350

351-
let isPathsReplacerPresent =
352-
config.plugins
353-
|> List.contains Constants.PerlaPathsReplacerPluginName
354-
355351
let defaultPlugins = seq {
356-
if isPathsReplacerPresent || not(Map.isEmpty config.paths) then
357-
ImportMaps.createPathsReplacerPlugin
358-
(AVal.constant config.paths)
359-
vfsOutputDir
360-
361352
if isEsbuildPluginPresent then
362353
args.EsbuildService.GetPlugin config.esbuild
363354
}

src/Perla/Esbuild.fs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type EsbuildService =
4646

4747
[<RequireQualifiedAccess>]
4848
module Esbuild =
49+
open FsToolkit.ErrorHandling
4950

5051
let singleFileCmd
5152
(
@@ -82,6 +83,45 @@ module Esbuild =
8283
}
8384

8485
let Create(serviceArgs: EsbuildServiceArgs) =
86+
let extractedAliasedPaths =
87+
serviceArgs.PerlaFsManager.PerlaConfiguration |> AVal.map (fun cfg ->
88+
let customPaths =
89+
cfg.paths |> Map.toSeq |> Seq.choose(fun (key: string<BareImport>, value: string<ResolutionUrl>) ->
90+
result {
91+
let key = UMX.untag key
92+
let value = UMX.untag value
93+
do!
94+
(System.IO.Path.IsPathRooted value || System.IO.Path.IsPathFullyQualified value)
95+
|> Result.requireFalse
96+
$"Path '{value}' must not be absolute or fully qualified."
97+
do! (value.StartsWith "./" || value.StartsWith "../")
98+
|> Result.requireTrue
99+
$"Path '{value}' must be relative (starting with './' or '../')."
100+
101+
let key =
102+
if key.EndsWith "/" then $"{key}*" else key
103+
104+
let value = if value.EndsWith "/" then [$"{value}*"] else [value]
105+
106+
return key, value
107+
108+
}
109+
|> Result.toOption
110+
)
111+
customPaths
112+
)
113+
let getOrBuildTsConfig paths =
114+
serviceArgs.PerlaFsManager.ResolveTsConfig
115+
|> AVal.map2
116+
(fun paths tsconfig ->
117+
match tsconfig with
118+
| None ->
119+
let paths = paths |> Map.ofSeq
120+
if paths |> Map.isEmpty then None else
121+
{| compilerOptions = {| paths = paths |} |} |> Json.Json.ToText |> Some
122+
| Some config -> Some config)
123+
paths
124+
85125
{ new EsbuildService with
86126
member this.GetPlugin(config: EsbuildConfig) : PluginInfo =
87127
let shouldTransform: FilePredicate =
@@ -101,8 +141,7 @@ module Esbuild =
101141
| ".js" -> None
102142
| _ -> None
103143

104-
let tsConfig =
105-
serviceArgs.PerlaFsManager.ResolveTsConfig |> AVal.force
144+
let tsConfig = getOrBuildTsConfig extractedAliasedPaths |> AVal.force
106145

107146
let! result =
108147
singleFileCmd(
@@ -152,13 +191,15 @@ module Esbuild =
152191
: CancellableTask<unit> =
153192
cancellableTask {
154193
let esbuildPath = serviceArgs.PerlaFsManager.ResolveEsbuildPath()
194+
let tsConfig = getOrBuildTsConfig extractedAliasedPaths |> AVal.force
155195

156196
do!
157197
serviceArgs.PlatformOps.RunEsbuildJs(
158198
esbuildPath,
159199
sourcesPath,
160200
UMX.untag entrypoint,
161201
UMX.untag outdir,
202+
tsConfig,
162203
config
163204
)
164205

src/Perla/FileSystem.fs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,21 @@ module FileSystem =
235235
}
236236

237237
member _.ResolveTsConfig = adaptive {
238-
let path =
238+
let tsConfigPath =
239239
args.PerlaDirectories.CurrentWorkingDirectory |/ "tsconfig.json"
240240

241-
let! content = AdaptiveFile.TryReadAllText(UMX.untag path)
241+
let jsConfigPath =
242+
args.PerlaDirectories.CurrentWorkingDirectory |/ "jsconfig.json"
243+
244+
let! content = adaptive {
245+
let! tsconfig = AdaptiveFile.TryReadAllText(UMX.untag tsConfigPath)
246+
match tsconfig with
247+
| Some tsconfig -> return Some tsconfig
248+
| None ->
249+
let! jsconfig = AdaptiveFile.TryReadAllText(UMX.untag jsConfigPath)
250+
return jsconfig
251+
252+
}
242253
return content
243254
}
244255

src/Perla/ImportMaps.fs

Lines changed: 2 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type ImportMapResolution = {
1717

1818
module ImportMaps =
1919
open System
20+
open System.IO
21+
open System.Collections.Generic
2022
// Checks if a path is a relative path (starts with ./ or ../ or similar patterns)
2123
let isRelativePath(path: string) =
2224
// Normalize separators to forward slashes for consistency
@@ -83,139 +85,6 @@ module ImportMaps =
8385
|> Map.filter(fun _k v -> not(isLocalImport mounts v))
8486
}
8587

86-
/// Replaces module names in import statements using the provided paths map.
87-
/// Returns the modified code string with replacements applied.
88-
let replaceImports
89-
(paths: Map<string<BareImport>, string<ResolutionUrl>>)
90-
(importingFile: string)
91-
(sourcesRoot: string<SystemPath>)
92-
(content: string)
93-
: string =
94-
let pattern =
95-
"import\\s+(?:.+?\\s+from\\s+['\"]([^'\"]+)['\"]|['\"]([^'\"]+)['\"])|import\\s*\\(\\s*(['\"])([^'\"]+)\\3\\s*([,)])"
96-
97-
let extractModuleName(m: Match) : string =
98-
if m.Groups[1].Success then m.Groups[1].Value
99-
elif m.Groups[2].Success then m.Groups[2].Value
100-
elif m.Groups[4].Success then m.Groups[4].Value
101-
else ""
102-
103-
let computeRelativeImport
104-
(importingDir: string)
105-
(replacementStr: string)
106-
(rest: string)
107-
: string =
108-
let target =
109-
if replacementStr.StartsWith("./") then
110-
replacementStr.Substring(2)
111-
elif replacementStr.StartsWith("../") then
112-
replacementStr
113-
else
114-
replacementStr
115-
// Compute absolute paths
116-
let importingDirAbs =
117-
let dir =
118-
if String.IsNullOrWhiteSpace(importingDir) then
119-
UMX.untag sourcesRoot
120-
elif System.IO.Path.IsPathRooted(importingDir) then
121-
importingDir
122-
else
123-
System.IO.Path.Combine(UMX.untag sourcesRoot, importingDir)
124-
125-
System.IO.Path.GetFullPath(dir)
126-
127-
let targetAbs =
128-
System.IO.Path.GetFullPath(
129-
System.IO.Path.Combine(UMX.untag sourcesRoot, target)
130-
)
131-
132-
let rel =
133-
System.IO.Path
134-
.GetRelativePath(importingDirAbs, targetAbs)
135-
.Replace('\\', '/')
136-
137-
let rel =
138-
if rel.StartsWith(".") || rel.StartsWith("/") then
139-
rel
140-
else
141-
"./" + rel
142-
143-
let rel = rel.TrimEnd('/')
144-
let rest = rest.TrimStart('/')
145-
if rest = "" then rel else rel + "/" + rest
146-
147-
let computeNewImport
148-
(importingDir: string)
149-
(moduleName: string)
150-
(prefixStr: string)
151-
(replacementStr: string)
152-
: string =
153-
let rest = moduleName.Substring(prefixStr.Length)
154-
155-
if
156-
isRelativePath replacementStr
157-
&& not(String.IsNullOrWhiteSpace importingDir)
158-
then
159-
computeRelativeImport importingDir replacementStr rest
160-
else
161-
replacementStr + rest
162-
163-
let replaceMatch(m: Match) : string =
164-
let moduleName = extractModuleName m
165-
166-
// Ensure importingFile is absolute, fallback to sourcesRoot if not
167-
let importingFileAbs =
168-
if System.IO.Path.IsPathRooted(importingFile) then
169-
importingFile
170-
else
171-
System.IO.Path.Combine(UMX.untag sourcesRoot, importingFile)
172-
|> System.IO.Path.GetFullPath
173-
174-
let importingDir =
175-
System.IO.Path.GetDirectoryName(importingFileAbs) |> nonNull
176-
177-
let tryReplace
178-
(prefix: string<BareImport>, replacement: string<ResolutionUrl>)
179-
: string option =
180-
let prefixStr, replacementStr = UMX.untag prefix, UMX.untag replacement
181-
182-
if moduleName.StartsWith(prefixStr) then
183-
let newImport =
184-
computeNewImport importingDir moduleName prefixStr replacementStr
185-
186-
Some(m.Value.Replace(moduleName, newImport))
187-
else
188-
None
189-
190-
paths
191-
|> Map.toSeq
192-
|> Seq.tryPick tryReplace
193-
|> Option.defaultValue m.Value
194-
195-
Regex.Replace(content, pattern, replaceMatch)
196-
197-
/// Creates a PluginInfo for the perla-paths-replacer-plugin
198-
let createPathsReplacerPlugin
199-
(pathsA: Map<string<BareImport>, string<ResolutionUrl>> aval)
200-
(sourcesRoot: string<SystemPath>)
201-
: Perla.Plugins.PluginInfo =
202-
let shouldTransform ext =
203-
[ ".js"; ".ts"; ".jsx"; ".tsx" ] |> List.contains ext
204-
205-
let transform: Plugins.Transform =
206-
fun file ->
207-
let paths = pathsA |> AVal.force
208-
209-
let replaced =
210-
replaceImports paths file.fileLocation sourcesRoot file.content
211-
212-
{ file with content = replaced }
213-
214-
plugin Constants.PerlaPathsReplacerPluginName {
215-
should_process_file shouldTransform
216-
with_transform transform
217-
}
218-
21988
/// Extracts all external import specifiers from an ImportMap (from both imports and scopes)
22089
let getExternals(importMap: ImportMap) =
22190
let importKeys = importMap.imports |> Map.toSeq |> Seq.map fst

src/Perla/PlatformOps.fs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type PlatformOps =
7979
workingDir: string<SystemPath> *
8080
entrypoint: string *
8181
outdir: string *
82+
tsconfig: string option *
8283
config: EsbuildConfig ->
8384
CancellableTask<unit>
8485

@@ -407,7 +408,7 @@ module PlatformOps =
407408
}
408409

409410
member _.RunEsbuildJs
410-
(esbuildPath, workingDir, entrypoint, outdir, config)
411+
(esbuildPath, workingDir, entrypoint, outdir,tsconfig, config)
411412
=
412413
cancellableTask {
413414
let! token = CancellableTask.getCancellationToken()
@@ -420,6 +421,10 @@ module PlatformOps =
420421
)
421422

422423
let output = Path.Combine(outdir, entrypoint)
424+
let addTsConfig (args: Builders.ArgumentsBuilder) =
425+
match tsconfig with
426+
| Some tsConfig -> args.Add($"--tsconfig-raw={tsConfig}")
427+
| None -> args
423428

424429
let command =
425430
Cli
@@ -434,6 +439,7 @@ module PlatformOps =
434439
|> buildEsbuildConfig config
435440
|> _.Add($"--outfile={output}")
436441
|> _.Add("--preserve-symlinks")
442+
|> addTsConfig
437443
|> buildEsbuildFileLoaders config.fileLoaders
438444
|> ignore)
439445
.WithValidation

src/Perla/PlatformOps.fsi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type PlatformOps =
7171
workingDir: string<SystemPath> *
7272
entrypoint: string *
7373
outdir: string *
74+
tsconfig: string option *
7475
config: EsbuildConfig ->
7576
CancellableTask<unit>
7677

0 commit comments

Comments
 (0)