Add config extends and entry point exclusion

Profiles may inherit from another config file (extends: <path>): the
first profile of the referenced file is merged underneath (scalars from
the overlay win, slices concatenate base-first and de-duplicate, chains
resolve recursively, cycles are detected).

esbuild.excludeEntryPoints removes merged entry points by glob (** spans
path separators, * stays within a segment), applied after path
resolution so patterns and entries match in the same shape.

Lets product editions/flavors (e.g. the wmd4x Range Trader Pro edition)
build only their own entry set on top of a shared, merge-clean base
config. Includes unit tests and template docs.
This commit is contained in:
pk
2026-10-11 15:58:56 +02:00
parent dca493bfdc
commit 49c88da564
5 changed files with 365 additions and 16 deletions
+71 -15
View File
@@ -38,20 +38,22 @@ func cfgToESBuildCfg(cfg options) api.BuildOptions {
}
type options struct {
Extends string `yaml:"extends"` // inherit from the first profile of the referenced config file
ESBuild struct {
EntryPoints []string `yaml:"entryPoints"`
Outdir string `yaml:"outdir"`
Outbase string `yaml:"outbase"`
Outfile string `yaml:"outfile"`
Sourcemap int `yaml:"sourcemap"`
Format int `yaml:"format"`
Splitting bool `yaml:"splitting"`
Platform int `yaml:"platform"`
Bundle bool `yaml:"bundle"`
Write bool `yaml:"write"`
LogLevel int `yaml:"logLevel"`
Target int `yaml:"target"`
PurgeBeforeBuild bool `yaml:"purgeBeforeBuild"`
EntryPoints []string `yaml:"entryPoints"`
ExcludeEntryPoints []string `yaml:"excludeEntryPoints"`
Outdir string `yaml:"outdir"`
Outbase string `yaml:"outbase"`
Outfile string `yaml:"outfile"`
Sourcemap int `yaml:"sourcemap"`
Format int `yaml:"format"`
Splitting bool `yaml:"splitting"`
Platform int `yaml:"platform"`
Bundle bool `yaml:"bundle"`
Write bool `yaml:"write"`
LogLevel int `yaml:"logLevel"`
Target int `yaml:"target"`
PurgeBeforeBuild bool `yaml:"purgeBeforeBuild"`
} `yaml:"esbuild"`
Watch struct {
Paths []string `yaml:"paths"`
@@ -119,6 +121,38 @@ func readCfg(cfgPath string) []options {
cfgPath = yamlPath
}
profiles := readCfgFile(cfgPath, map[string]bool{})
// Path resolution and entry point filtering run once, after all extends
// chains are merged, so de-duplication compares like with like and
// patterns match the same shape as the entries.
for i := range profiles {
processPaths(&profiles[i])
filterEntryPoints(&profiles[i])
}
return profiles
}
// readCfgFile loads a config file and resolves `extends` chains. A profile
// may declare `extends: <path>`; the first profile of the referenced file is
// merged underneath it (scalars: overlay wins when set, slices: concatenated
// base-first and de-duplicated). Chains are resolved recursively; visiting
// guards against inheritance cycles.
func readCfgFile(cfgPath string, visiting map[string]bool) []options {
absPath, err := filepath.Abs(cfgPath)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
if visiting[absPath] {
fmt.Printf("extends cycle detected at %s\n", absPath)
os.Exit(1)
}
visiting[absPath] = true
defer delete(visiting, absPath)
cfgContent, err := os.ReadFile(cfgPath)
if err != nil {
@@ -140,9 +174,27 @@ func readCfg(cfgPath string) []options {
optsSetups = append(optsSetups, opt)
}
// Process all paths in each options setup
cfgDir := filepath.Dir(cfgPath)
for i := range optsSetups {
processPaths(&optsSetups[i])
base := strings.TrimSpace(optsSetups[i].Extends)
if base == "" {
continue
}
if !filepath.IsAbs(base) {
base = filepath.Join(cfgDir, base)
}
baseProfiles := readCfgFile(base, visiting)
if len(baseProfiles) == 0 {
fmt.Printf("extends target %s contains no profiles\n", base)
os.Exit(1)
}
optsSetups[i] = mergeOptions(baseProfiles[0], optsSetups[i])
}
return optsSetups
@@ -178,6 +230,10 @@ func processPaths(opts *options) {
for i, entry := range opts.ESBuild.EntryPoints {
opts.ESBuild.EntryPoints[i] = fsutils.ResolvePath(entry)
}
for i, entry := range opts.ESBuild.ExcludeEntryPoints {
opts.ESBuild.ExcludeEntryPoints[i] = fsutils.ResolvePath(entry)
}
opts.ESBuild.Outdir = fsutils.ResolvePath(opts.ESBuild.Outdir)
opts.ESBuild.Outfile = fsutils.ResolvePath(opts.ESBuild.Outfile)