Skip to content

Commit 5c8373d

Browse files
authored
fix: enable usage of config files when package.json type=module (#8455)
1 parent 054fad8 commit 5c8373d

19 files changed

+248
-90
lines changed

.changeset/smart-impalas-explain.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"app-builder-lib": patch
3+
"electron-builder": patch
4+
---
5+
6+
fix: allow usage of "module" typ config files

packages/app-builder-lib/package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@
5858
"builder-util": "workspace:*",
5959
"builder-util-runtime": "workspace:*",
6060
"chromium-pickle-js": "^0.2.0",
61+
"config-file-ts": "0.2.8-rc1",
6162
"debug": "^4.3.4",
63+
"dotenv": "^16.4.5",
64+
"dotenv-expand": "^11.0.6",
6265
"ejs": "^3.1.8",
6366
"electron-publish": "workspace:*",
6467
"form-data": "^4.0.0",
@@ -67,9 +70,9 @@
6770
"is-ci": "^3.0.0",
6871
"isbinaryfile": "^5.0.0",
6972
"js-yaml": "^4.1.0",
73+
"json5": "^2.2.3",
7074
"lazy-val": "^1.0.5",
7175
"minimatch": "^10.0.0",
72-
"read-config-file": "6.4.0",
7376
"resedit": "^1.7.0",
7477
"sanitize-filename": "^1.6.3",
7578
"semver": "^7.3.8",
@@ -105,7 +108,8 @@
105108
"@types/semver": "7.3.8",
106109
"@types/tar": "^6.1.3",
107110
"dmg-builder": "workspace:*",
108-
"electron-builder-squirrel-windows": "workspace:*"
111+
"electron-builder-squirrel-windows": "workspace:*",
112+
"toml": "^3.0.0"
109113
},
110114
"peerDependencies": {
111115
"dmg-builder": "workspace:*",

packages/app-builder-lib/src/codeSign/windowsCodeSign.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { computeToolEnv, ToolInfo } from "../util/bundledTool"
66
import { rename } from "fs-extra"
77
import * as os from "os"
88
import * as path from "path"
9-
import { resolveFunction } from "../platformPackager"
9+
import { resolveFunction } from "../util/resolve"
1010
import { isUseSystemSigncode } from "../util/flags"
1111
import { VmManager } from "../vm/vm"
1212
import { WinPackager } from "../winPackager"

packages/app-builder-lib/src/electron/electronVersion.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { httpExecutor } from "builder-util"
66
import { readJson } from "fs-extra"
77
import { Lazy } from "lazy-val"
88
import * as path from "path"
9-
import { orNullIfFileNotExist } from "read-config-file"
9+
import { orNullIfFileNotExist } from "../util/config/load"
1010
import * as semver from "semver"
1111
import { Configuration } from "../configuration"
12-
import { getConfig } from "../util/config"
12+
import { getConfig } from "../util/config/config"
1313

1414
export type MetadataValue = Lazy<{ [key: string]: any } | null>
1515

packages/app-builder-lib/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { log, InvalidConfigurationError, executeFinally } from "builder-util"
33
import { asArray } from "builder-util-runtime"
44
import { Packager } from "./packager"
55
import { PackagerOptions } from "./packagerApi"
6-
import { resolveFunction } from "./platformPackager"
6+
import { resolveFunction } from "./util/resolve"
77
import { PublishManager } from "./publish/PublishManager"
88

99
export { Packager, BuildResult } from "./packager"

packages/app-builder-lib/src/macPackager.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { DIR_TARGET, Platform, Target } from "./core"
1010
import { AfterPackContext, ElectronPlatformName } from "./index"
1111
import { MacConfiguration, MasConfiguration, NotarizeNotaryOptions } from "./options/macOptions"
1212
import { Packager } from "./packager"
13-
import { chooseNotNull, resolveFunction, PlatformPackager } from "./platformPackager"
13+
import { chooseNotNull, PlatformPackager } from "./platformPackager"
1414
import { ArchiveTarget } from "./targets/ArchiveTarget"
1515
import { PkgTarget, prepareProductBuildArgs } from "./targets/pkg"
1616
import { createCommonTarget, NoOpTarget } from "./targets/targetFactory"
@@ -20,6 +20,7 @@ import * as fs from "fs/promises"
2020
import { notarize } from "@electron/notarize"
2121
import { NotarizeOptionsNotaryTool, NotaryToolKeychainCredentials } from "@electron/notarize/lib/types"
2222
import { MemoLazy } from "builder-util-runtime"
23+
import { resolveFunction } from "./util/resolve"
2324

2425
export type CustomMacSignOptions = SignOptions
2526
export type CustomMacSign = (configuration: CustomMacSignOptions, packager: MacPackager) => Promise<void>

packages/app-builder-lib/src/packager.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@ import { Framework } from "./Framework"
2929
import { LibUiFramework } from "./frameworks/LibUiFramework"
3030
import { Metadata } from "./options/metadata"
3131
import { ArtifactBuildStarted, ArtifactCreated, PackagerOptions } from "./packagerApi"
32-
import { PlatformPackager, resolveFunction } from "./platformPackager"
32+
import { PlatformPackager } from "./platformPackager"
3333
import { ProtonFramework } from "./ProtonFramework"
3434
import { computeArchToTargetNamesMap, createTargets, NoOpTarget } from "./targets/targetFactory"
35-
import { computeDefaultAppDirectory, getConfig, validateConfiguration } from "./util/config"
35+
import { computeDefaultAppDirectory, getConfig, validateConfiguration } from "./util/config/config"
3636
import { expandMacro } from "./util/macroExpander"
3737
import { createLazyProductionDeps, NodeModuleDirInfo, NodeModuleInfo } from "./util/packageDependencies"
3838
import { checkMetadata, readPackageJson } from "./util/packageMetadata"
3939
import { getRepositoryInfo } from "./util/repositoryInfo"
4040
import { installOrRebuild, nodeGypRebuild } from "./util/yarn"
4141
import { PACKAGE_VERSION } from "./version"
4242
import { release as getOsRelease } from "os"
43+
import { resolveFunction } from "./util/resolve"
4344

4445
function addHandler(emitter: EventEmitter, event: string, handler: (...args: Array<any>) => void) {
4546
emitter.on(event, handler)

packages/app-builder-lib/src/platformPackager.ts

+2-47
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import BluebirdPromise from "bluebird-lst"
2-
import { Arch, asArray, AsyncTaskManager, debug, DebugLogger, deepAssign, getArchSuffix, InvalidConfigurationError, isEmptyOrSpaces, log } from "builder-util"
2+
import { Arch, asArray, AsyncTaskManager, DebugLogger, deepAssign, getArchSuffix, InvalidConfigurationError, isEmptyOrSpaces, log } from "builder-util"
33
import { defaultArchFromString, getArtifactArchName, FileTransformer, statOrNull, orIfFileNotExist } from "builder-util"
44
import { readdir } from "fs/promises"
55
import { Lazy } from "lazy-val"
66
import { Minimatch } from "minimatch"
77
import * as path from "path"
8-
import { pathToFileURL } from "url"
98
import { AppInfo } from "./appInfo"
109
import { checkFileInArchive } from "./asar/asarFileChecker"
1110
import { AsarPackager } from "./asar/asarUtil"
@@ -30,6 +29,7 @@ import {
3029
import { executeAppBuilderAsJson } from "./util/appBuilder"
3130
import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier"
3231
import { expandMacro as doExpandMacro } from "./util/macroExpander"
32+
import { resolveFunction } from "./util/resolve"
3333

3434
export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions> {
3535
get packagerOptions(): PackagerOptions {
@@ -770,51 +770,6 @@ export function normalizeExt(ext: string) {
770770
return ext.startsWith(".") ? ext.substring(1) : ext
771771
}
772772

773-
async function resolveModule<T>(type: string | undefined, name: string): Promise<T> {
774-
const extension = path.extname(name).toLowerCase()
775-
const isModuleType = type === "module"
776-
try {
777-
if (extension === ".mjs" || (extension === ".js" && isModuleType)) {
778-
const fileUrl = pathToFileURL(name).href
779-
return await eval("import('" + fileUrl + "')")
780-
}
781-
} catch (error: any) {
782-
log.debug({ moduleName: name, message: error.message ?? error.stack }, "Unable to dynamically import hook, falling back to `require`")
783-
}
784-
try {
785-
return require(name)
786-
} catch (error: any) {
787-
log.error({ moduleName: name, message: error.message ?? error.stack }, "Unable to `require` hook")
788-
throw new Error(error.message ?? error.stack)
789-
}
790-
}
791-
792-
export async function resolveFunction<T>(type: string | undefined, executor: T | string, name: string): Promise<T> {
793-
if (executor == null || typeof executor !== "string") {
794-
return executor
795-
}
796-
797-
let p = executor as string
798-
if (p.startsWith(".")) {
799-
p = path.resolve(p)
800-
}
801-
802-
try {
803-
p = require.resolve(p)
804-
} catch (e: any) {
805-
debug(e)
806-
p = path.resolve(p)
807-
}
808-
809-
const m: any = await resolveModule(type, p)
810-
const namedExport = m[name]
811-
if (namedExport == null) {
812-
return m.default || m
813-
} else {
814-
return namedExport
815-
}
816-
}
817-
818773
export function chooseNotNull(v1: string | null | undefined, v2: string | null | undefined): string | null | undefined {
819774
return v1 == null ? v2 : v1
820775
}

packages/app-builder-lib/src/util/NodeModuleCopyHelper.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { lstat, readdir, lstatSync } from "fs-extra"
44
import * as path from "path"
55
import { excludedNames, FileMatcher } from "../fileMatcher"
66
import { Packager } from "../packager"
7-
import { resolveFunction } from "../platformPackager"
7+
import { resolveFunction } from "./resolve"
88
import { FileCopyHelper } from "./AppFileWalker"
99
import { NodeModuleInfo } from "./packageDependencies"
1010
import { realpathSync } from "fs"

packages/app-builder-lib/src/util/config.ts packages/app-builder-lib/src/util/config/config.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { DebugLogger, deepAssign, InvalidConfigurationError, log, safeStringifyJ
22
import { readJson } from "fs-extra"
33
import { Lazy } from "lazy-val"
44
import * as path from "path"
5-
import { getConfig as _getConfig, loadParentConfig, orNullIfFileNotExist, ReadConfigRequest } from "read-config-file"
6-
import { Configuration } from "../configuration"
7-
import { FileSet } from "../options/PlatformSpecificBuildOptions"
8-
import { reactCra } from "../presets/rectCra"
9-
import { PACKAGE_VERSION } from "../version"
5+
import { getConfig as _getConfig, loadParentConfig, orNullIfFileNotExist, ReadConfigRequest } from "./load"
6+
import { Configuration } from "../../configuration"
7+
import { FileSet } from "../../options/PlatformSpecificBuildOptions"
8+
import { reactCra } from "../../presets/rectCra"
9+
import { PACKAGE_VERSION } from "../../version"
1010
// eslint-disable-next-line @typescript-eslint/no-var-requires
1111
const validateSchema = require("@develar/schema-utils")
1212

@@ -214,7 +214,7 @@ function getDefaultConfig(): Configuration {
214214
}
215215
}
216216

217-
const schemeDataPromise = new Lazy(() => readJson(path.join(__dirname, "..", "..", "scheme.json")))
217+
const schemeDataPromise = new Lazy(() => readJson(path.join(__dirname, "..", "..", "..", "scheme.json")))
218218

219219
export async function validateConfiguration(config: Configuration, debugLogger: DebugLogger) {
220220
const extraMetadata = config.extraMetadata
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { promises as fs } from "fs"
2+
import { load } from "js-yaml"
3+
import * as path from "path"
4+
import { Lazy } from "lazy-val"
5+
import { parse as parseEnv } from "dotenv"
6+
import { loadTsConfig } from "config-file-ts"
7+
import { DotenvParseInput, expand } from "dotenv-expand"
8+
import { resolveModule } from "../resolve"
9+
import { log } from "builder-util"
10+
11+
export interface ReadConfigResult<T> {
12+
readonly result: T
13+
readonly configFile: string | null
14+
}
15+
16+
async function readConfig<T>(configFile: string, request: ReadConfigRequest): Promise<ReadConfigResult<T>> {
17+
const data = await fs.readFile(configFile, "utf8")
18+
let result: any
19+
if (configFile.endsWith(".json5") || configFile.endsWith(".json")) {
20+
result = require("json5").parse(data)
21+
} else if (configFile.endsWith(".js") || configFile.endsWith(".cjs" || configFile.endsWith(".mjs"))) {
22+
const json = await orNullIfFileNotExist(fs.readFile(path.join(process.cwd(), "package.json"), "utf8"))
23+
const moduleType = json === null ? null : JSON.parse(json).type
24+
result = await resolveModule(moduleType, configFile)
25+
if (result.default != null) {
26+
result = result.default
27+
}
28+
if (typeof result === "function") {
29+
result = result(request)
30+
}
31+
result = await Promise.resolve(result)
32+
} else if (configFile.endsWith(".ts")) {
33+
result = loadTsConfig(configFile)
34+
if (typeof result === "function") {
35+
result = result(request)
36+
}
37+
result = await Promise.resolve(result)
38+
} else if (configFile.endsWith(".toml")) {
39+
result = require("toml").parse(data)
40+
} else {
41+
result = load(data)
42+
}
43+
return { result, configFile }
44+
}
45+
46+
export async function findAndReadConfig<T>(request: ReadConfigRequest): Promise<ReadConfigResult<T> | null> {
47+
const prefix = request.configFilename
48+
for (const configFile of [`${prefix}.yml`, `${prefix}.yaml`, `${prefix}.json`, `${prefix}.json5`, `${prefix}.toml`, `${prefix}.js`, `${prefix}.cjs`, `${prefix}.ts`]) {
49+
const data = await orNullIfFileNotExist(readConfig<T>(path.join(request.projectDir, configFile), request))
50+
if (data != null) {
51+
return data
52+
}
53+
}
54+
55+
return null
56+
}
57+
58+
export function orNullIfFileNotExist<T>(promise: Promise<T>): Promise<T | null> {
59+
return orIfFileNotExist(promise, null)
60+
}
61+
62+
export function orIfFileNotExist<T>(promise: Promise<T>, fallbackValue: T): Promise<T> {
63+
return promise.catch(e => {
64+
if (e.code === "ENOENT" || e.code === "ENOTDIR") {
65+
return fallbackValue
66+
}
67+
throw e
68+
})
69+
}
70+
71+
export interface ReadConfigRequest {
72+
packageKey: string
73+
configFilename: string
74+
75+
projectDir: string
76+
packageMetadata: Lazy<{ [key: string]: any } | null> | null
77+
}
78+
79+
export async function loadConfig<T>(request: ReadConfigRequest): Promise<ReadConfigResult<T> | null> {
80+
let packageMetadata = request.packageMetadata == null ? null : await request.packageMetadata.value
81+
if (packageMetadata == null) {
82+
const json = await orNullIfFileNotExist(fs.readFile(path.join(request.projectDir, "package.json"), "utf8"))
83+
packageMetadata = json == null ? null : JSON.parse(json)
84+
}
85+
86+
const data: T = packageMetadata == null ? null : packageMetadata[request.packageKey]
87+
return data == null ? findAndReadConfig<T>(request) : { result: data, configFile: null }
88+
}
89+
90+
export function getConfig<T>(request: ReadConfigRequest, configPath?: string | null): Promise<ReadConfigResult<T> | null> {
91+
if (configPath == null) {
92+
return loadConfig<T>(request)
93+
} else {
94+
return readConfig<T>(path.resolve(request.projectDir, configPath), request)
95+
}
96+
}
97+
98+
export async function loadParentConfig<T>(request: ReadConfigRequest, spec: string): Promise<ReadConfigResult<T>> {
99+
let isFileSpec: boolean | undefined
100+
if (spec.startsWith("file:")) {
101+
spec = spec.substring("file:".length)
102+
isFileSpec = true
103+
}
104+
105+
let parentConfig = await orNullIfFileNotExist(readConfig<T>(path.resolve(request.projectDir, spec), request))
106+
if (parentConfig == null && isFileSpec !== true) {
107+
let resolved: string | null = null
108+
try {
109+
resolved = require.resolve(spec)
110+
} catch (e) {
111+
// ignore
112+
}
113+
114+
if (resolved != null) {
115+
parentConfig = await readConfig<T>(resolved, request)
116+
}
117+
}
118+
119+
if (parentConfig == null) {
120+
throw new Error(`Cannot find parent config file: ${spec}`)
121+
}
122+
123+
return parentConfig
124+
}
125+
126+
export async function loadEnv(envFile: string) {
127+
const data = await orNullIfFileNotExist(fs.readFile(envFile, "utf8"))
128+
if (data == null) {
129+
return null
130+
}
131+
132+
const parsed = parseEnv<DotenvParseInput>(data)
133+
134+
log.info({ envFile }, "injecting environment")
135+
Object.entries(parsed).forEach(([key, value]) => {
136+
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
137+
process.env[key] = value
138+
}
139+
})
140+
expand({ parsed })
141+
return parsed
142+
}

0 commit comments

Comments
 (0)