Implement zodify, compile, and non-source phases

This commit is contained in:
Garrett Mills 2022-01-09 11:26:20 -06:00
parent 46128ff9af
commit af8b5ff875
13 changed files with 1137 additions and 47 deletions

View File

@ -1,3 +1,3 @@
# template-npm-typescript
# @extollo/cc
A template repository for NPM packages built with Typescript and PNPM.
Early-phase compiler for Extollo projects.

View File

@ -1,7 +1,7 @@
{
"name": "template-npm-typescript",
"name": "@extollo/cc",
"version": "0.1.0",
"description": "A template for NPM packages built with TypeScript",
"description": "Early-phase compiler for Extollo projects",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"directories": {
@ -17,11 +17,14 @@
"files": [
"lib/**/*"
],
"bin": {
"excc": "lib/excc.js"
},
"prepare": "pnpm run build",
"postversion": "git push && git push --tags",
"repository": {
"type": "git",
"url": "https://code.garrettmills.dev/garrettmills/template-npm-typescript"
"url": "https://code.garrettmills.dev/extollo/cc"
},
"author": "Garrett Mills <shout@garrettmills.dev>",
"license": "MIT",
@ -31,12 +34,21 @@
"eslint": "^8.2.0"
},
"dependencies": {
"@types/argparse": "^2.0.10",
"@types/cli-color": "^2.0.2",
"@types/fs-extra": "^9.0.13",
"@types/mkdirp": "^1.0.2",
"@types/rimraf": "^3.0.2",
"@types/uuid": "^8.3.3",
"argparse": "^2.0.1",
"cli-color": "^2.0.1",
"dotenv": "^10.0.0",
"fs-extra": "^10.0.0",
"mkdirp": "^1.0.4",
"rfdc": "^1.3.0",
"rimraf": "^3.0.2",
"ts-node": "^10.4.0",
"ts-to-zod": "^1.8.0",
"typescript": "^4.5.2",
"uuid": "^8.3.2"
}

File diff suppressed because it is too large Load Diff

4
src/ExCompileError.ts Normal file
View File

@ -0,0 +1,4 @@
export class ExCompileError extends Error {
}

46
src/ExCompiler.ts Normal file
View File

@ -0,0 +1,46 @@
import {ExtolloCompileConfig} from './types'
import {Phase} from './phases/Phase'
import {PreparePhase} from './phases/PreparePhase'
import {ZodifyPhase} from './phases/ZodifyPhase'
import {CompilePhase} from './phases/CompilePhase'
import {NonSourcePhase} from './phases/NonSourcePhase'
import {Logger} from './Logger'
export class ExCompiler {
protected phases: Phase[] = []
constructor(
protected tsconfigPath: string,
protected outputDirectory: string,
protected tsconfig: any,
protected config: ExtolloCompileConfig,
) {
this.initialize()
}
protected initialize(): void {
this.phases.push(new PreparePhase(this.config, this.tsconfig))
if ( this.config.zodify ) {
for ( const zodPath of this.config.zodify ) {
this.phases.push(new ZodifyPhase(this.config, this.tsconfig, zodPath))
}
}
this.phases.push(new CompilePhase(this.config, this.tsconfig, this.tsconfigPath))
if ( this.config['non-source'] ) {
for ( const nonSourcePath of this.config['non-source'] ) {
this.phases.push(new NonSourcePhase(this.config, this.tsconfig, nonSourcePath))
}
}
}
public async run(): Promise<void> {
Logger.info('Start compile...')
for ( const phase of this.phases ) {
await phase.run()
}
Logger.success('Compiled successfully!')
}
}

29
src/Logger.ts Normal file
View File

@ -0,0 +1,29 @@
import * as clc from 'cli-color'
export abstract class Logger {
private static verbose = false
public static setVerbosity(verbose: boolean) {
this.verbose = verbose
}
public static info(...out: any) {
this.log(clc.blue('info '), ...out)
}
public static error(...out: any) {
this.log(clc.red('error '), ...out)
}
public static success(...out: any) {
this.log(clc.green('success '), ...out)
}
public static verb(...out: any) {
this.log(clc.italic('verb '), ...out)
}
private static log(...out: any) {
console.log(...out) // eslint-disable-line no-console
}
}

51
src/excc.ts Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env node
// Read config
// Create build phases
// Copy base files to compile directory
// Run initial phases
// Run compilation into output directory
import { ArgumentParser } from 'argparse'
import { ExCompiler } from './ExCompiler'
import {Logger} from './Logger'
import * as path from 'path'
(async () => {
const parser = new ArgumentParser({
description: 'Early-phase compiler for Extollo projects',
})
parser.add_argument('-c', '--config', {
help: 'path to the package.json of the project to compile',
required: true,
})
parser.add_argument('-t', '--tsconfig', {
help: 'path to the tsconfig.json for the project to compile',
required: true,
})
parser.add_argument('-v', '--verbose', {
help: 'output more verbose and debugging output',
action: 'store_true',
})
const args = parser.parse_args()
Logger.setVerbosity(Boolean(args.verbose))
const tsconfig = await import(path.resolve(args.tsconfig))
const packageJson = await import(path.resolve(args.config))
const config = packageJson?.extollo?.cc ?? {}
const cc = new ExCompiler(
args.tsconfig,
tsconfig?.compilerOptions?.outDir || './lib',
tsconfig,
config,
)
await cc.run()
})()

View File

@ -0,0 +1,38 @@
import * as childProcess from 'child_process'
import {Phase} from './Phase'
import {ExtolloCompileConfig} from '../types'
import {Logger} from '../Logger'
import {ExCompileError} from '../ExCompileError'
export class CompilePhase extends Phase {
constructor(
config: ExtolloCompileConfig,
tsconfig: any,
protected readonly tsconfigPath: string,
) {
super(config, tsconfig)
}
public run(): Promise<void> {
Logger.verb('tsc', 'transpile sources')
const tsc = childProcess.spawn('tsc', ['-p', this.tsconfigPath])
tsc.stdout.on('data', output => {
Logger.info('tsc', output)
})
tsc.stderr.on('data', output => {
Logger.info('tsc', output)
})
return new Promise((res, rej) => {
tsc.on('exit', code => {
if ( code === 0 ) {
res()
} else {
rej(new ExCompileError('Subprocess exited with non-zero exit code'))
}
})
})
}
}

View File

@ -0,0 +1,52 @@
import {Phase} from './Phase'
import {ExtolloCompileConfig} from '../types'
import * as fse from 'fs-extra'
import * as path from 'path'
import {Logger} from '../Logger'
export class NonSourcePhase extends Phase {
constructor(
config: ExtolloCompileConfig,
tsconfig: any,
protected readonly nonSourcePath: string,
) {
super(config, tsconfig)
}
async run(): Promise<void> {
const outDir = this.tsconfig?.compilerOptions?.outDir || './lib'
const source = this.nonSourcePath
let dest = path.join(outDir, source)
if ( this.shouldUp(source) ) {
const upLevel = path.join(outDir).split(path.sep).length
dest = path.join(
outDir,
source.split(path.sep)
.slice(upLevel)
.join(path.sep),
)
}
const destParentDir = path.join(dest, '..')
Logger.verb('non-source', 'ensure', destParentDir)
await fse.mkdirp(destParentDir)
Logger.verb('non-source', 'copy', source, '->', dest)
await fse.copy(source, dest)
}
public shouldUp(nonSourcePath: string): boolean {
if ( Array.isArray(this.tsconfig.include) ) {
for ( const sourcePath of this.tsconfig.include ) {
if ( nonSourcePath.startsWith(sourcePath) ) {
return true
}
}
}
return false
}
}

10
src/phases/Phase.ts Normal file
View File

@ -0,0 +1,10 @@
import {ExtolloCompileConfig} from '../types'
export abstract class Phase {
constructor(
protected readonly config: ExtolloCompileConfig,
protected readonly tsconfig: any,
) { }
public abstract run(): void | Promise<void>;
}

View File

@ -0,0 +1,42 @@
import * as fse from 'fs-extra'
import * as path from 'path'
import * as mkdirp from 'mkdirp'
import * as rimraf from 'rimraf'
import * as rfdc from 'rfdc'
import { Phase } from './Phase'
import { Logger } from '../Logger'
export class PreparePhase extends Phase {
public async run(): Promise<void> {
const dir = this.config.compileDir || 'exbuild'
Logger.verb('prepare', `remove ${dir}`)
await new Promise<void>((res, rej) => {
rimraf(dir, e => {
if ( e ) {
rej(e)
} else {
res()
}
})
})
Logger.verb('prepare', `create ${dir}`)
await mkdirp(dir)
for ( const src of this.tsconfig.include ) {
Logger.verb('prepare', `copy ${src}`)
await fse.copy(src, path.join(dir, src))
}
const tsconfig = rfdc()(this.tsconfig)
const outDir = tsconfig?.compilerOptions?.outDir || './lib'
if ( !tsconfig.compilerOptions ) {
tsconfig.compilerOptions = {}
}
tsconfig.compilerOptions.outDir = path.join('..', outDir)
fse.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfig, undefined, 4))
}
}

78
src/phases/ZodifyPhase.ts Normal file
View File

@ -0,0 +1,78 @@
import {Phase} from './Phase'
import {ExtolloCompileConfig} from '../types'
import * as rimraf from 'rimraf'
import * as fse from 'fs-extra'
import * as path from 'path'
import {Logger} from '../Logger'
async function* walk(dir: string): any {
for await (const d of await fse.promises.opendir(dir)) {
const entry = path.join(dir, d.name)
if (d.isDirectory()) {
yield* walk(entry)
} else if (d.isFile()) {
yield entry
}
}
}
export class ZodifyPhase extends Phase {
constructor(
config: ExtolloCompileConfig,
tsconfig: any,
protected readonly zodPath: string,
) {
super(config, tsconfig)
}
public async run(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsz = require('ts-to-zod')
const dir = this.config.compileDir || 'exbuild'
await new Promise<void>((res, rej) => {
rimraf(path.resolve(dir, this.zodPath), e => e ? rej(e) : res())
})
await fse.mkdirp(path.resolve(dir, this.zodPath))
for await ( const file of walk(this.zodPath) ) {
if ( !file.endsWith('.ts') ) {
continue
}
Logger.verb('zod', file)
const sourceText = (await fse.readFile(file)).toString('utf-8')
const gen = tsz.generate({
sourceText,
getSchemaName: () => 'exZodifiedSchema',
})
const zodSource = gen.getZodSchemasFile(file)
const augmentedSource = this.getAugmentedSourceText(sourceText, zodSource)
await fse.writeFile(path.resolve(dir, file), augmentedSource)
}
}
protected getAugmentedSourceText(originalSource: string, zodSource: string): string {
const lines = originalSource.split('\n')
let line = 0
if ( lines[line].startsWith('#!') ) {
if ( lines[line + 1] ) {
line = line + 1
}
}
const newlines: string[] = []
lines.forEach((lineVal, idx) => {
if ( idx === line ) {
newlines.push('import { z } from "zod";')
}
newlines.push(lineVal)
})
return [...newlines, ...zodSource.split('\n').slice(2)].join('\n')
}
}

6
src/types.ts Normal file
View File

@ -0,0 +1,6 @@
export interface ExtolloCompileConfig {
compileDir?: string,
zodify?: string[],
'non-source': string[],
}