From 147365afd4eb8852d7576609eab496a2ee6685b5 Mon Sep 17 00:00:00 2001 From: Jacob Schneider Date: Fri, 13 Sep 2024 17:23:40 +0200 Subject: [PATCH] Changed to cross-platform build-system --- bin/build.js | 57 ++++++++++++++++++++++++++++ bin/components.js | 89 +++++++++++++++++++++++++++++++++++++++++++ bin/log.js | 39 +++++++++++++++++++ bin/path.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++ bin/util.js | 30 +++++++++++++++ makefile.json5 | 38 ------------------- package.json | 12 ++---- 7 files changed, 315 insertions(+), 47 deletions(-) create mode 100644 bin/build.js create mode 100644 bin/components.js create mode 100644 bin/log.js create mode 100644 bin/path.js create mode 100644 bin/util.js delete mode 100644 makefile.json5 diff --git a/bin/build.js b/bin/build.js new file mode 100644 index 0000000..c00fee8 --- /dev/null +++ b/bin/build.js @@ -0,0 +1,57 @@ +import * as fs from 'node:fs/promises'; +import State from '@j-cake/jcake-utils/state'; +import { iterSync } from '@j-cake/jcake-utils/iter'; +import * as Format from '@j-cake/jcake-utils/args'; +import chalk from 'chalk'; + +import log from './log.js'; +import Path from './path.js'; +import * as comp from './components.js'; + +export const config = new State({ + logLevel: 'info', + force: false, + + root: new Path(process.cwd()), + out: new Path(process.cwd()).concat('build'), + + components: [] +}); + +export default async function main(argv) { + const logLevel = Format.oneOf(Object.keys(log), false); + + for (const { current: i, skip: next } of iterSync.peekable(argv)) + if (i == '--log-level') + config.setState({ logLevel: logLevel(next()) }); + + else if (i == '-f' || i == '--force') + config.setState({ force: true }); + + else if (i == '-o' || i == '--out') + config.setState({ out: new Path(next()) }); + + else + config.setState(prev => ({ components: [...prev.components, i] })); + + log.debug(config.get()); + + await fs.mkdir(config.get().out.path, { recursive: true }); + + for (const component of config.get().components) + if (component in components) + await components[component]() + .then(status => log.info(`${chalk.grey(component)}: Done`)); +} + +export const components = { + "build:plugin": () => comp.build_plugin(), + "build:package.json": () => comp.build_package_json(), + "build:manifest.json": () => comp.build_manifest_json(), + "build:style.css": () => comp.build_style_css(), + + "phony:install": () => comp.phony_install(), + "phony:all": () => Promise.all(Object.entries(components) + .filter(([comp, _]) => comp.startsWith("build:")) + .map(([_, fn]) => fn())), +} diff --git a/bin/components.js b/bin/components.js new file mode 100644 index 0000000..20b41b8 --- /dev/null +++ b/bin/components.js @@ -0,0 +1,89 @@ +import * as fs from 'node:fs/promises'; +import * as fss from 'node:fs'; +import * as cp from 'node:child_process'; +import esbuild from 'esbuild'; + +import { config } from './build.js'; +import log from './log.js'; +import { has_changed } from './util.js'; + +const is_source = path => path.startsWith(config.get().root.join("src")); + +export async function build_plugin() { + if (!await has_changed({ + glob: path => is_source(path), + dependents: [config.get().out.join("main.js")] + })) + return log.verbose("Skipping Rebuild"); + + await esbuild.build({ + entryPoints: ["src/main.ts"], + bundle: true, + sourcemap: true, + platform: 'node', + format: 'cjs', + external: ['electron', 'obsidian'], + outdir: config.get().out.path + }); +} + +export async function build_package_json() { + if (!await has_changed({ + glob: path => is_source(path), + dependents: [config.get().out.join("package.json")] + })) + return log.verbose("Skipping Rebuild"); + + const jq = cp.spawn('jq', ['-r', '. * .deploy * {deploy:null} | with_entries(select(.value |. != null))']); + + fss.createReadStream(config.get().root.join("package.json").path) + .pipe(jq.stdin); + + jq.stdout.pipe(fss.createWriteStream(config.get().out.join("package.json").path), 'utf8'); + + await new Promise((ok, err) => jq.on("exit", code => code == 0 ? ok() : err(code))); +} + +export async function build_manifest_json() { + if (!await has_changed({ + glob: path => is_source(path), + dependents: [config.get().out.join("manifest.json")] + })) + return log.verbose("Skipping Rebuild"); + + const jq = cp.spawn('jq', ['-r', '.']); + + fss.createReadStream(config.get().root.join("manifest.json").path) + .pipe(jq.stdin); + + jq.stdout.pipe(fss.createWriteStream(config.get().out.join("manifest.json").path), 'utf8'); + + await new Promise((ok, err) => jq.on("exit", code => code == 0 ? ok() : err(code))); +} + +export async function build_style_css() { + if (!await has_changed({ + glob: path => is_source(path), + dependents: [config.get().out.join("styles.css")] + })) + return log.verbose("Skipping Rebuild"); + + await esbuild.build({ + entryPoints: ["styles.css"], + bundle: true, + sourcemap: true, + outdir: config.get().out.path + }); +} + +export async function phony_install() { + const pkg = await fs.readFile(config.get().root.join("package.json")) + .then(pkg => JSON.parse(pkg).name); + + const install = config.get().vault.join(".obsidian/plugins").join(pkg).path; + + await fs.mkdir(install, { recursive: true }); + + for await (const file of config.get().out.readdir()) + await fs.copyFile(file.path, install); +} diff --git a/bin/log.js b/bin/log.js new file mode 100644 index 0000000..016a82e --- /dev/null +++ b/bin/log.js @@ -0,0 +1,39 @@ +import util from 'node:util'; +import chalk from 'chalk'; + +import { config } from './build.js'; + +export const stripAnsi = str => str.replace(/[\u001b\u009b][[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[\dA-ORZcf-nqry=><]/g, ''); +export const centre = (text, width) => { + const colourless = stripAnsi(text); + const pad = Math.floor((width - colourless.length) / 2); + return `${' '.repeat(pad)}${text}${' '.repeat(pad)}`.padStart(width, ' '); +} +export function stdout(tag, ...msg) { + const log = msg + .map(i => ['string', 'number', 'bigint', 'boolean'].includes(typeof i) ? i : util.inspect(i, false, null, true)) + .join(' ') + .split('\n') + .map((i, a) => `${a ? centre('\u2502', stripAnsi(tag).length) : tag} ${i}\n`); + + for (const i of log) + process.stdout.write(i); +} + +export function stderr(tag, ...msg) { + const log = msg + .map(i => ['string', 'number', 'bigint', 'boolean'].includes(typeof i) ? i : util.inspect(i, false, null, true)) + .join(' ') + .split('\n') + .map((i, a) => `${a ? centre('\u2502', stripAnsi(tag).length) : tag} ${i}\n`); + + for (const i of log) + process.stderr.write(i); +} + +export default { + err: (...arg) => void (['err', 'info', 'verbose', 'debug'].includes(config.get().logLevel) && stderr(chalk.grey(`[${chalk.red('Error')}]`), ...arg)), + info: (...arg) => void (['info', 'verbose', 'debug'].includes(config.get().logLevel) && stdout(chalk.grey(`[${chalk.blue('Info')}]`), ...arg)), + verbose: (...arg) => void (['verbose', 'debug'].includes(config.get().logLevel) && stdout(chalk.grey(`[${chalk.yellow('Verbose')}]`), ...arg)), + debug: (...arg) => void (['debug'].includes(config.get().logLevel) && stdout(chalk.grey(`[${chalk.cyan('Debug')}]`), ...arg)) +} \ No newline at end of file diff --git a/bin/path.js b/bin/path.js new file mode 100644 index 0000000..0b1a24b --- /dev/null +++ b/bin/path.js @@ -0,0 +1,97 @@ +import * as fs from 'node:fs/promises'; +import * as fss from 'node:fs'; +import * as path from 'node:path'; + +import log from './log.js'; + +export default class Path { + constructor(str) { + if (str instanceof fss.Dirent) + this.path = path.join(str.parentPath, str.name); + else if (str instanceof Path) + this.path = str.path; + else + this.path = path.isAbsolute(str) ? str : path.join(process.cwd(), str); + } + + concat(...paths) { + this.path = path.join(this.path, ...paths.map(i => i instanceof Path ? i.path : i)); + return this; + } + + join(...paths) { + return new Path(path.join(this.path, ...paths.map(i => i instanceof Path ? i.path : i))); + } + + ext() { + return this.path.split(path.sep).pop()?.split('.').pop()?.toLowerCase() ?? ''; + } + + async *readdir(recursive = true) { + const files = await fs.readdir(this.path, { recursive, withFileTypes: true }) + + for (const dirent of files) + yield new Path(dirent); + + // for await (const dir of await fs.readdir(config.get().root.path, { recursive: true, withFileTypes: true })) + // if (dir.isFile()) + // log.info(new Path(dir)); + } + + async mtime() { + return await fs.stat(this.path).then(stat => stat.mtime); + } + + async isFile() { + return await fs.stat(this.path).then(stat => stat.isFile()); + } + + async isDir() { + return await fs.stat(this.path).then(stat => stat.isDirectory()); + } + + async isBlockdev() { + return await fs.stat(this.path).then(stat => stat.isBlockDevice()); + } + + async isChardev() { + return await fs.stat(this.path).then(stat => stat.isCharacterDevice()); + } + + async isPipe() { + return await fs.stat(this.path).then(stat => stat.isFIFO() || stat.isSocket()); + } + + async isFifo() { + return await fs.stat(this.path).then(stat => stat.isFIFO()); + } + + async isSocket() { + return await fs.stat(this.path).then(stat => stat.isSocket()); + } + + async isSymlink() { + return await fs.stat(this.path).then(stat => stat.isSymbolicLink()); + } + + replaceBase(base, newBase) { + if (this.path.startsWith(base.path)) + return newBase.join(this.path.slice(base.path.length)); + + else + throw { + err: 'Could not substitute path', + reason: `Path does not start with '${base.path}'`, + path: this + }; + } + + parent() { + return new Path(this.path.split(path.sep).slice(0, -1).join(path.sep)); + } + + startsWith(base) { + const chunks = this.path.split(path.sep); + return base.path.split(path.sep).every(i => chunks.shift() == i); + } +} \ No newline at end of file diff --git a/bin/util.js b/bin/util.js new file mode 100644 index 0000000..1d1d230 --- /dev/null +++ b/bin/util.js @@ -0,0 +1,30 @@ +import { config } from './build.js'; + +const file_tree = async function() { + const files = []; + + if (files.length == 0) + for await (const path of config.get().root.readdir()) + files.push(path); + + return files; +} + +export async function* get_files(glob) { + for (const path of await file_tree()) + if (await glob(path)) + yield path; +} + +export async function has_changed(glob) { + if (config.get().force) + return true; + + const mtimes = await Promise.all(glob.dependents.map(i => i.mtime().catch(_ => new Date(0)))); + + for await (const file of get_files(glob.glob)) + if (await Promise.race(mtimes.map(async i => await file.mtime() > i))) + return true; + + return false; +} diff --git a/makefile.json5 b/makefile.json5 deleted file mode 100644 index b5bda1d..0000000 --- a/makefile.json5 +++ /dev/null @@ -1,38 +0,0 @@ -{ - env: { - PATH: "echo $PWD/node_modules/.bin:$PATH" - }, - targets: { - 'build/main.js': { - dependencies: ['src/*.ts'], - run: "esbuild src/main.ts --outdir=build --bundle --sourcemap --platform=node --format=cjs --external:obsidian --external:electron" - }, - 'build/package.json': { - dependencies: ['package.json', 'makefile.json5'], - run: "cat package.json | jq -r '. * .deploy * {deploy:null} | with_entries(select(.value |. != null))' > build/package.json" - }, - 'build/manifest.json': { - dependencies: ['manifest.json', 'package.json'], - run: "cat manifest.json | jq -r '.' > build/manifest.json" - }, - 'build/styles.css': { - dependencies: ['styles.css'], - run: "cp styles.css build/styles.css" - }, - - // phony - - clean: { - phony: true, - run: "rm -rf build node_modules *lock* *yarn* *pnpm*" - }, - - install: { - phony: true, - run: [ - "mkdir -p \"$vault_dir/.obsidian/plugins/\"", - "cp -r $PWD/build \"$vault_dir/.obsidian/plugins/$(cat package.json | jq -r '.name')\"" - ] - } - } -} diff --git a/package.json b/package.json index 9870832..458674b 100644 --- a/package.json +++ b/package.json @@ -14,21 +14,15 @@ "chrono-node": "https://github.com/wanasit/chrono.git" }, "scripts": { - "build:plugin": "esbuild src/main.ts --outdir=build --bundle --sourcemap --platform=node --format=cjs --external:obsidian --external:electron", - "build:package.json": "cat package.json | jq -r '. * .deploy * {deploy:null} | with_entries(select(.value |. != null))' > build/package.json", - "build:manifest.json": "cat manifest.json | jq -r '.' > build/manifest.json", - "build:styles.css": "esbuild styles.css --outdir=build --bundle", - "build:hotreload": "echo hotreload > build/.hotreload", - "phony:rebuild": "cat package.json | jq -r '.scripts | keys_unsorted[] | select(. | startswith(\"build:\"))' | xargs -d \\\\n -I {} $npm_execpath run {}", - "phony:install": "mkdir -p \"$vault_dir/.obsidian/plugins/$(cat package.json | jq -r '.name')\"; cp -ra build/. \"$vault_dir/.obsidian/plugins/$(cat package.json | jq -r '.name')\"", - "phony:clean": "rm -rf build target node_modules *lock* *yarn* *pnpm*" + "build": "node -e \"await import('./bin/build.js').then(script => script.default(process.argv.slice(1)))\" --" }, "devDependencies": { "@j-cake/mkjson": "latest", - "@types/luxon": "^3.4.2", + "@types/luxon": "latest", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", + "chalk": "latest", "electron": "latest", "esbuild": "latest", "obsidian": "latest",