Skip to content

Commit

Permalink
feat: add support for forge.config.ts et. al
Browse files Browse the repository at this point in the history
This brings in rechoir and interpret which allow arbitrary extensions / loaders for our forge config.

Notably this allows us to make forge.config.ts a thing (and thus generate type safe configurations).
  • Loading branch information
MarshallOfSound committed Oct 26, 2022
1 parent 247bcc8 commit 8d83840
Show file tree
Hide file tree
Showing 29 changed files with 173 additions and 89 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"got": "^11.8.5",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^8.0.0",
"interpret": "^3.1.1",
"lodash": "^4.17.20",
"log-symbols": "^4.0.0",
"mime-types": "^2.1.25",
Expand All @@ -73,6 +74,7 @@
"parse-author": "^2.0.0",
"pretty-ms": "^7.0.0",
"progress": "^2.0.3",
"rechoir": "^0.8.0",
"resolve-package": "^1.0.1",
"semver": "^7.2.1",
"source-map-support": "^0.5.13",
Expand Down Expand Up @@ -102,6 +104,7 @@
"@types/fetch-mock": "^7.3.1",
"@types/fs-extra": "^9.0.6",
"@types/inquirer": "^8.1.1",
"@types/interpret": "^1.1.1",
"@types/listr": "^0.14.2",
"@types/lodash": "^4.14.166",
"@types/mime-types": "^2.1.0",
Expand All @@ -111,6 +114,7 @@
"@types/node-fetch": "^2.5.5",
"@types/progress": "^2.0.5",
"@types/proxyquire": "^1.3.28",
"@types/rechoir": "^0.6.1",
"@types/semver": "^7.3.4",
"@types/sinon": "^10.0.0",
"@types/sinon-chai": "^3.2.5",
Expand Down
4 changes: 4 additions & 0 deletions packages/api/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"@electron-forge/maker-wix": "6.0.0-beta.68",
"@electron-forge/maker-zip": "6.0.0-beta.68",
"@electron-forge/test-utils": "6.0.0-beta.68",
"@types/interpret": "^1.1.1",
"@types/progress": "^2.0.5",
"@types/rechoir": "^0.6.1",
"chai": "^4.3.3",
"chai-as-promised": "^7.0.0",
"cross-env": "^7.0.2",
Expand Down Expand Up @@ -56,10 +58,12 @@
"find-up": "^5.0.0",
"fs-extra": "^10.0.0",
"got": "^11.8.5",
"interpret": "^3.1.1",
"lodash": "^4.17.20",
"log-symbols": "^4.0.0",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"rechoir": "^0.8.0",
"resolve-package": "^1.0.1",
"semver": "^7.2.1",
"source-map-support": "^0.5.13",
Expand Down
6 changes: 3 additions & 3 deletions packages/api/core/src/api/make.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'path';

import { asyncOra } from '@electron-forge/async-ora';
import MakerBase from '@electron-forge/maker-base';
import { ForgeArch, ForgeConfig, ForgeConfigMaker, ForgeMakeResult, ForgePlatform, IForgeResolvableMaker } from '@electron-forge/shared-types';
import { ForgeArch, ForgeConfigMaker, ForgeMakeResult, ForgePlatform, IForgeResolvableMaker, ResolvedForgeConfig } from '@electron-forge/shared-types';
import { getHostArch } from '@electron/get';
import chalk from 'chalk';
import filenamify from 'filenamify';
Expand All @@ -29,7 +29,7 @@ class MakerImpl extends MakerBase<any> {

type MakeTargets = ForgeConfigMaker[] | string[];

function generateTargets(forgeConfig: ForgeConfig, overrideTargets?: MakeTargets) {
function generateTargets(forgeConfig: ResolvedForgeConfig, overrideTargets?: MakeTargets) {
if (overrideTargets) {
return overrideTargets.map((target) => {
if (typeof target === 'string') {
Expand Down Expand Up @@ -90,7 +90,7 @@ export default async ({
}: MakeOptions): Promise<ForgeMakeResult[]> => {
asyncOra.interactive = interactive;

let forgeConfig!: ForgeConfig;
let forgeConfig!: ResolvedForgeConfig;
await asyncOra('Resolving Forge Config', async () => {
const resolvedDir = await resolveDir(dir);
if (!resolvedDir) {
Expand Down
35 changes: 23 additions & 12 deletions packages/api/core/src/util/forge-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import path from 'path';

import { ForgeConfig, IForgeResolvableMaker } from '@electron-forge/shared-types';
import { ForgeConfig, IForgeResolvableMaker, ResolvedForgeConfig } from '@electron-forge/shared-types';
import fs from 'fs-extra';
import * as interpret from 'interpret';
import { template } from 'lodash';
import * as rechoir from 'rechoir';

import { runMutatingHook } from './hook';
import PluginInterface from './plugin-interface';
Expand Down Expand Up @@ -119,22 +121,29 @@ export function renderConfigTemplate(dir: string, templateObj: any, obj: any): v
}
}

export default async (dir: string): Promise<ForgeConfig> => {
type MaybeESM<T> = T | { default: T };

export default async (dir: string): Promise<ResolvedForgeConfig> => {
const packageJSON = await readRawPackageJson(dir);
let forgeConfig: ForgeConfig | string | null = packageJSON.config && packageJSON.config.forge ? packageJSON.config.forge : null;

if (!forgeConfig) {
if (await fs.pathExists(path.resolve(dir, 'forge.config.js'))) {
forgeConfig = 'forge.config.js';
} else {
forgeConfig = {} as ForgeConfig;
for (const extension of ['.js', ...Object.keys(interpret.extensions)]) {
const pathToConfig = path.resolve(dir, `forge.config${extension}`);
if (await fs.pathExists(pathToConfig)) {
rechoir.prepare(interpret.extensions, pathToConfig, dir);
forgeConfig = `forge.config${extension}`;
break;
}
}
}
forgeConfig = forgeConfig || ({} as ForgeConfig);

if (await forgeConfigIsValidFilePath(dir, forgeConfig)) {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
forgeConfig = require(path.resolve(dir, forgeConfig as string)) as ForgeConfig;
const loaded = require(path.resolve(dir, forgeConfig as string)) as MaybeESM<ForgeConfig>;
forgeConfig = 'default' in loaded ? loaded.default : loaded;
} catch (err) {
console.error(`Failed to load: ${path.resolve(dir, forgeConfig as string)}`);
throw err;
Expand All @@ -149,17 +158,19 @@ export default async (dir: string): Promise<ForgeConfig> => {
publishers: [],
plugins: [],
};
forgeConfig = {
let resolvedForgeConfig: ResolvedForgeConfig = {
...defaultForgeConfig,
...forgeConfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pluginInterface: null as any,
};

const templateObj = { ...packageJSON, year: new Date().getFullYear() };
renderConfigTemplate(dir, templateObj, forgeConfig);
renderConfigTemplate(dir, templateObj, resolvedForgeConfig);

forgeConfig.pluginInterface = new PluginInterface(dir, forgeConfig);
resolvedForgeConfig.pluginInterface = new PluginInterface(dir, resolvedForgeConfig);

forgeConfig = await runMutatingHook(forgeConfig, 'resolveForgeConfig', forgeConfig);
resolvedForgeConfig = await runMutatingHook(resolvedForgeConfig, 'resolveForgeConfig', resolvedForgeConfig);

return proxify<ForgeConfig>(forgeConfig.buildIdentifier || '', forgeConfig, 'ELECTRON_FORGE');
return proxify<ResolvedForgeConfig>(resolvedForgeConfig.buildIdentifier || '', resolvedForgeConfig, 'ELECTRON_FORGE');
};
6 changes: 3 additions & 3 deletions packages/api/core/src/util/hook.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import debug from 'debug';

const d = debug('electron-forge:hook');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const runHook = async (forgeConfig: ForgeConfig, hookName: string, ...hookArgs: any[]): Promise<void> => {
export const runHook = async (forgeConfig: ResolvedForgeConfig, hookName: string, ...hookArgs: any[]): Promise<void> => {
const { hooks } = forgeConfig;
if (hooks) {
d(`hook triggered: ${hookName}`);
Expand All @@ -16,7 +16,7 @@ export const runHook = async (forgeConfig: ForgeConfig, hookName: string, ...hoo
await forgeConfig.pluginInterface.triggerHook(hookName, hookArgs);
};

export async function runMutatingHook<T>(forgeConfig: ForgeConfig, hookName: string, item: T): Promise<T> {
export async function runMutatingHook<T>(forgeConfig: ResolvedForgeConfig, hookName: string, item: T): Promise<T> {
const { hooks } = forgeConfig;
if (hooks) {
d(`hook triggered: ${hookName}`);
Expand Down
4 changes: 2 additions & 2 deletions packages/api/core/src/util/out-dir.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from 'path';

import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';

const BASE_OUT_DIR = 'out';

export default (baseDir: string, forgeConfig: ForgeConfig): string => {
export default (baseDir: string, forgeConfig: ResolvedForgeConfig): string => {
if (forgeConfig.buildIdentifier) {
let identifier = forgeConfig.buildIdentifier;
if (typeof identifier === 'function') {
Expand Down
6 changes: 3 additions & 3 deletions packages/api/core/src/util/plugin-interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PluginBase from '@electron-forge/plugin-base';
import { ForgeConfig, IForgePlugin, IForgePluginInterface, StartResult } from '@electron-forge/shared-types';
import { IForgePlugin, IForgePluginInterface, ResolvedForgeConfig, StartResult } from '@electron-forge/shared-types';
import debug from 'debug';

import { StartOptions } from '../api';
Expand All @@ -15,9 +15,9 @@ function isForgePlugin(plugin: IForgePlugin | unknown): plugin is IForgePlugin {
export default class PluginInterface implements IForgePluginInterface {
private plugins: IForgePlugin[];

private config: ForgeConfig;
private config: ResolvedForgeConfig;

constructor(dir: string, forgeConfig: ForgeConfig) {
constructor(dir: string, forgeConfig: ResolvedForgeConfig) {
this.plugins = forgeConfig.plugins.map((plugin) => {
if (isForgePlugin(plugin)) {
return plugin;
Expand Down
4 changes: 2 additions & 2 deletions packages/api/core/src/util/read-package-json.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';

import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import fs from 'fs-extra';

import { runMutatingHook } from './hook';
Expand All @@ -9,5 +9,5 @@ import { runMutatingHook } from './hook';
export const readRawPackageJson = async (dir: string): Promise<any> => fs.readJson(path.resolve(dir, 'package.json'));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const readMutatedPackageJson = async (dir: string, forgeConfig: ForgeConfig): Promise<any> =>
export const readMutatedPackageJson = async (dir: string, forgeConfig: ResolvedForgeConfig): Promise<any> =>
runMutatingHook(forgeConfig, 'readPackageJson', await readRawPackageJson(dir));
24 changes: 15 additions & 9 deletions packages/api/core/test/fast/forge-config_spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';

import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { expect } from 'chai';

import findConfig, {
Expand Down Expand Up @@ -106,21 +106,27 @@ describe('forge-config', () => {
});

it('should resolve the JS file exports in config.forge points to a JS file and maintain functions', async () => {
type MagicFunctionConfig = ForgeConfig & { magicFn: () => string };
type MagicFunctionConfig = ResolvedForgeConfig & { magicFn: () => string };
const conf = (await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf'))) as MagicFunctionConfig;
expect(conf.magicFn).to.be.a('function');
expect(conf.magicFn()).to.be.equal('magic result');
});

it('should resolve the JS file exports of forge.config.js if config.forge does not exist points', async () => {
type DefaultResolvedConfig = ForgeConfig & { defaultResolved: boolean };
it('should resolve the JS file exports of forge.config.js if config.forge does not exist ', async () => {
type DefaultResolvedConfig = ResolvedForgeConfig & { defaultResolved: boolean };
const conf = (await findConfig(path.resolve(__dirname, '../fixture/dummy_default_js_conf'))) as DefaultResolvedConfig;
expect(conf.buildIdentifier).to.equal('default');
expect(conf.defaultResolved).to.equal(true);
});

it('should resolve the TS file exports of forge.config.ts if config.forge does not exist and the TS config exists', async () => {
type DefaultResolvedConfig = ResolvedForgeConfig;
const conf = (await findConfig(path.resolve(__dirname, '../fixture/dummy_default_ts_conf'))) as DefaultResolvedConfig;
expect(conf.buildIdentifier).to.equal('typescript');
});

it('should magically map properties to environment variables', async () => {
type MappedConfig = ForgeConfig & {
type MappedConfig = ResolvedForgeConfig & {
s3: {
secretAccessKey?: string;
};
Expand All @@ -140,7 +146,7 @@ describe('forge-config', () => {
});

it('should resolve values fromBuildIdentifier', async () => {
type ResolveBIConfig = ForgeConfig & {
type ResolveBIConfig = ResolvedForgeConfig & {
topLevelProp: string;
sub: {
prop: {
Expand All @@ -164,13 +170,13 @@ describe('forge-config', () => {
});

it('should resolve undefined from fromBuildIdentifier if no value is provided', async () => {
type ResolveUndefConfig = ForgeConfig & { topLevelUndef?: string };
type ResolveUndefConfig = ResolvedForgeConfig & { topLevelUndef?: string };
const conf = (await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf'))) as ResolveUndefConfig;
expect(conf.topLevelUndef).to.equal(undefined);
});

it('should leave arrays intact', async () => {
type NestedConfig = ForgeConfig & {
type NestedConfig = ResolvedForgeConfig & {
sub: {
prop: {
inArray: string[];
Expand All @@ -182,7 +188,7 @@ describe('forge-config', () => {
});

it('should leave regexps intact', async () => {
type RegExpConfig = ForgeConfig & { regexp: RegExp };
type RegExpConfig = ResolvedForgeConfig & { regexp: RegExp };
const conf = (await findConfig(path.resolve(__dirname, '../fixture/dummy_js_conf'))) as RegExpConfig;
expect(conf.regexp).to.be.instanceOf(RegExp);
expect(conf.regexp.test('foo')).to.equal(true, 'regexp should match foo');
Expand Down
4 changes: 2 additions & 2 deletions packages/api/core/test/fast/hook_spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ForgeConfig, ForgeHookFn } from '@electron-forge/shared-types';
import { ForgeHookFn, ResolvedForgeConfig } from '@electron-forge/shared-types';
import { expect } from 'chai';
import { SinonStub, stub } from 'sinon';

Expand All @@ -9,7 +9,7 @@ const fakeConfig = {
triggerHook: async () => false,
triggerMutatingHook: async (_hookName: string, item: unknown) => item,
},
} as unknown as ForgeConfig;
} as unknown as ResolvedForgeConfig;

describe('hooks', () => {
describe('runHook', () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/api/core/test/fast/out-dir_spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';

import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { expect } from 'chai';

import getCurrentOutDir from '../../src/util/out-dir';
Expand All @@ -10,22 +10,22 @@ describe('out-dir', () => {

describe('getCurrentOutDir', () => {
it('resolves to the default out directory when nothing extra is declared', () => {
expect(getCurrentOutDir(DIR, {} as ForgeConfig)).to.equal(`${DIR}${path.sep}out`);
expect(getCurrentOutDir(DIR, {} as ResolvedForgeConfig)).to.equal(`${DIR}${path.sep}out`);
});

it('resolves to the provided identifier', () => {
expect(
getCurrentOutDir(DIR, {
buildIdentifier: 'bar',
} as ForgeConfig)
} as ResolvedForgeConfig)
).to.equal(`${DIR}${path.sep}out${path.sep}bar`);
});

it('resolves to the return value of provided identifier getter', () => {
expect(
getCurrentOutDir(DIR, {
buildIdentifier: () => 'thing',
} as ForgeConfig)
} as ResolvedForgeConfig)
).to.equal(`${DIR}${path.sep}out${path.sep}thing`);
});
});
Expand Down
8 changes: 4 additions & 4 deletions packages/api/core/test/fast/read-package-json_spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import path from 'path';

import { ForgeConfig } from '@electron-forge/shared-types';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { expect } from 'chai';

import { readMutatedPackageJson, readRawPackageJson } from '../../src/util/read-package-json';

const emptyForgeConfig: Partial<ForgeConfig> = {
const emptyForgeConfig: Partial<ResolvedForgeConfig> = {
packagerConfig: {},
rebuildConfig: {},
makers: [],
Expand All @@ -31,7 +31,7 @@ describe('read-package-json', () => {
triggerHook: () => Promise.resolve(),
overrideStartLogic: () => Promise.resolve(false),
},
} as ForgeConfig)
} as ResolvedForgeConfig)
).to.deep.equal(require('../../package.json'));
});

Expand All @@ -44,7 +44,7 @@ describe('read-package-json', () => {
triggerHook: () => Promise.resolve(),
overrideStartLogic: () => Promise.resolve(false),
},
} as ForgeConfig)
} as ResolvedForgeConfig)
).to.deep.equal('test_mut');
});
});
Expand Down
3 changes: 3 additions & 0 deletions packages/api/core/test/fast/upgrade-forge-config_spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import assert from 'assert';

import { ForgeConfig, IForgeResolvableMaker, IForgeResolvablePublisher } from '@electron-forge/shared-types';
import { expect } from 'chai';
import { merge } from 'lodash';
Expand Down Expand Up @@ -104,6 +106,7 @@ describe('upgradeForgeConfig', () => {
};
const newConfig = upgradeForgeConfig(oldConfig);
expect(newConfig.publishers).to.have.lengthOf(1);
assert(newConfig.publishers);
const publisherConfig = (newConfig.publishers[0] as IForgeResolvablePublisher).config;
expect(publisherConfig.repository).to.deep.equal(repo);
expect(publisherConfig.octokitOptions).to.deep.equal(octokitOptions);
Expand Down
Loading

0 comments on commit 8d83840

Please sign in to comment.