Skip to content

Commit

Permalink
feat: add builder dev mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ArcherGu committed May 30, 2022
1 parent e34d8f7 commit b2ef6dc
Show file tree
Hide file tree
Showing 14 changed files with 633 additions and 76 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ const config = {
},
overrides: [
{
files: ['packages/create-doubleshot/index.js', 'packages/runner/**/*.ts'],
files: ['packages/create-doubleshot/index.js', 'packages/runner/**/*.ts', 'packages/builder/**/*.ts'],
rules: {
'no-console': 'off'
}
},
{
files: ['packages/builder/**/*.ts'],
rules: {
'@typescript-eslint/no-var-requires': 'off'
}
}
],
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
}
}
}
14 changes: 14 additions & 0 deletions packages/builder/bin/dsb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node
'use strict'
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const resolveFrom = require('resolve-from')

let modulePath = '../dist/cli'
try {
// use local cli if exists
modulePath = path.join(path.dirname(resolveFrom(process.cwd(), '@doubleshot/builder')), 'cli.js')
}
catch { }

require(modulePath)
29 changes: 29 additions & 0 deletions packages/builder/node/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { cac } from 'cac'
import { version } from '../package.json'
import { createLogger } from './log'
const cli = cac('doubleshot-build')

cli
.option('-c, --config <file>', '[string] use specified config file')

// dev
cli
.command('', 'run in development mode') // default command
.alias('dev') // alias to align with the script name
.action(async () => {
const logger = createLogger()
const { dev } = await import('./dev')

try {
await dev()
}
catch (e) {
logger.error('DSB', e)
process.exit(1)
}
})

cli.help()
cli.version(version)

cli.parse()
134 changes: 134 additions & 0 deletions packages/builder/node/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import path from 'path'
import JoyCon from 'joycon'
import { bundleRequire } from 'bundle-require'
import type { Configuration as ElectronBuilderConfiguration } from 'electron-builder'
import type { Options as TsupOptions } from 'tsup'
import { merge, normalizePath } from './utils'
import { createLogger } from './log'

export type DoubleShotBuilderConfigExport = DoubleShotBuilderConfig | Promise<DoubleShotBuilderConfig>

export interface ElectronBuildConfig {
/**
* @default false
*/
disabled?: boolean
config?: string | ElectronBuilderConfiguration
afterBuild?: () => Promise<void>
}

export type TsupBuildConfig = Pick<TsupOptions, 'entry' | 'outDir' | 'tsconfig' | 'external'> & {
tsupConfig?: string | TsupOptions
}

export interface DoubleShotBuilderConfig extends TsupBuildConfig {
/**
* @default 'package.json'.main
*/
main?: string
electron?: {
preload?: TsupBuildConfig
build?: ElectronBuildConfig
}
afterBuild?: () => Promise<void>
}

export type ResolvedConfig = Readonly<{
cwd: string
configFile: string | undefined
tsupConfigs: TsupOptions[]
electronBuild?: ElectronBuildConfig
} & Pick<DoubleShotBuilderConfig, 'main' | 'afterBuild'>>

export function defineConfig(config: DoubleShotBuilderConfigExport): DoubleShotBuilderConfigExport {
return config
}

export async function resolveConfig(): Promise<ResolvedConfig> {
const logger = createLogger()
const cwd = process.cwd()
const configJoycon = new JoyCon()
const configPath = await configJoycon.resolve({
files: [
'dsb.config.ts',
'dsb.config.js',
'dsb.config.cjs',
'dsb.config.mjs',
],
cwd,
stopDir: path.parse(cwd).root,
})

if (configPath) {
logger.info('dsr', `Using doubleshot builder config: ${configPath}\n`)

const { mod } = await bundleRequire({
filepath: configPath,
})

const config: DoubleShotBuilderConfig = mod.default || mod

const mergeTsupConfig = async (inputConfig: TsupBuildConfig, defaultConfig: TsupOptions = {}) => {
let result: TsupOptions | undefined
if (inputConfig.tsupConfig) {
if (typeof inputConfig.tsupConfig === 'string') {
const tsupConfigPath = await configJoycon.resolve({
files: [inputConfig.tsupConfig],
cwd,
stopDir: path.parse(cwd).root,
})
if (!tsupConfigPath) {
logger.warn('DSB', `tsup config file: ${config.tsupConfig} not found, ignored.\n`)
}
else {
const { mod } = await bundleRequire({
filepath: tsupConfigPath,
})
result = mod.default || mod
}
}
else if (typeof inputConfig.tsupConfig === 'object') {
result = inputConfig.tsupConfig
}
}

const userTsupConfig: TsupOptions = merge(defaultConfig, {
entry: inputConfig.entry,
outDir: inputConfig.outDir,
tsconfig: inputConfig.tsconfig,
external: inputConfig.external,
})

return result ? { ...userTsupConfig, ...result } : userTsupConfig
}

const tsupConfigArr: TsupOptions[] = [(await mergeTsupConfig(config))]

if (config.electron?.preload)
tsupConfigArr.push(await mergeTsupConfig(config.electron.preload, tsupConfigArr[0]))

return {
cwd,
main: config.main ? normalizePath(path.resolve(cwd, config.main)) : undefined,
configFile: normalizePath(configPath),
tsupConfigs: tsupConfigArr,
electronBuild: resoleElectronBuilderConfig(config.electron?.build, cwd),
afterBuild: config.afterBuild,
}
}
else {
throw new Error('doubleshot builder needs a config file')
}
}

function resoleElectronBuilderConfig(buildConfig: ElectronBuildConfig | undefined, cwd: string): ElectronBuildConfig {
if (!buildConfig)
return { disabled: true }

const resolvedConfig = typeof buildConfig.config === 'string' ? normalizePath(path.resolve(cwd, buildConfig.config)) : buildConfig.config
return {
disabled: buildConfig.disabled === true,
config: resolvedConfig,
afterBuild: buildConfig.afterBuild,
}
}
46 changes: 46 additions & 0 deletions packages/builder/node/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { spawn } from 'child_process'
import path from 'path'
import fs from 'fs'
import * as colorette from 'colorette'
import { build as tsupBuild } from 'tsup'
import electron from 'electron'
import { resolveConfig } from './config'

function exitMainProcess() {
console.info(colorette.yellow('Main Process Exited'))
process.exit(0)
}

function runMainProcess(mainFile: string) {
return spawn(electron as any, [mainFile], { stdio: 'inherit' }).on('exit', exitMainProcess)
}

export async function dev() {
const config = await resolveConfig()

for (const tsupConfig of config.tsupConfigs) {
await tsupBuild({
...tsupConfig,
})
}

let mainFile = config.main
if (!mainFile) {
const file = path.resolve(config.cwd, 'package.json')
const data = require(file)
delete require.cache[file]

if (Object.prototype.hasOwnProperty.call(data, 'main'))
mainFile = path.resolve(config.cwd, data.main)

else
throw new Error('package.json missing main field')
}

if (!fs.existsSync(mainFile))
throw new Error(`Main File Not Found: ${mainFile}`)

console.info(colorette.blue(`Run Main File: ${mainFile}`))

runMainProcess(mainFile)
}
1 change: 1 addition & 0 deletions packages/builder/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './config'
77 changes: 77 additions & 0 deletions packages/builder/node/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as colors from 'colorette'

type LOG_TYPE = 'info' | 'success' | 'error' | 'warn'

export const colorize = (type: LOG_TYPE, data: any, onlyImportant = false) => {
if (onlyImportant && (type === 'info' || type === 'success'))
return data

const color
= type === 'info'
? 'blue'
: type === 'error'
? 'red'
: type === 'warn'
? 'yellow'
: 'green'
return colors[color](data)
}

export const makeLabel = (
name: string | undefined,
input: string,
type: LOG_TYPE,
) => {
return [
name && `${colors.dim('[')}${name.toUpperCase()}${colors.dim(']')}`,
colorize(type, input.toUpperCase()),
]
.filter(Boolean)
.join(' ')
}

export type Logger = ReturnType<typeof createLogger>

export const createLogger = (name?: string) => {
return {
setName(_name: string) {
name = _name
},

success(label: string, ...args: any[]) {
return this.log(label, 'success', ...args)
},

info(label: string, ...args: any[]) {
return this.log(label, 'info', ...args)
},

error(label: string, ...args: any[]) {
return this.log(label, 'error', ...args)
},

warn(label: string, ...args: any[]) {
return this.log(label, 'warn', ...args)
},

log(
label: string,
type: 'info' | 'success' | 'error' | 'warn',
...data: unknown[]
) {
switch (type) {
case 'error': {
return console.error(
makeLabel(name, label, type),
...data.map(item => colorize(type, item, true)),
)
}
default:
console.log(
makeLabel(name, label, type),
...data.map(item => colorize(type, item, true)),
)
}
},
}
}
25 changes: 25 additions & 0 deletions packages/builder/node/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import path from 'path'
import os from 'os'

export const isWindows = os.platform() === 'win32'

export function slash(p: string): string {
return p.replace(/\\/g, '/')
}

export function normalizePath(id: string): string {
return path.posix.normalize(isWindows ? slash(id) : id)
}

export function arraify<T>(target: T | T[]): T[] {
return Array.isArray(target) ? target : [target]
}

export function merge<T>(obj1: T, obj2: T): T {
const result = Object.assign({}, obj1)
for (const key in obj2) {
if (obj2[key] !== undefined)
result[key] = obj2[key]
}
return result
}
Loading

0 comments on commit b2ef6dc

Please sign in to comment.