generated from garrettmills/template-npm-typescript
Implement zodify, compile, and non-source phases
This commit is contained in:
parent
46128ff9af
commit
af8b5ff875
@ -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.
|
||||
|
18
package.json
18
package.json
@ -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"
|
||||
}
|
||||
|
806
pnpm-lock.yaml
806
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
4
src/ExCompileError.ts
Normal file
4
src/ExCompileError.ts
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
export class ExCompileError extends Error {
|
||||
|
||||
}
|
46
src/ExCompiler.ts
Normal file
46
src/ExCompiler.ts
Normal 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
29
src/Logger.ts
Normal 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
51
src/excc.ts
Normal 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()
|
||||
})()
|
||||
|
38
src/phases/CompilePhase.ts
Normal file
38
src/phases/CompilePhase.ts
Normal 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'))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
52
src/phases/NonSourcePhase.ts
Normal file
52
src/phases/NonSourcePhase.ts
Normal 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
10
src/phases/Phase.ts
Normal 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>;
|
||||
}
|
42
src/phases/PreparePhase.ts
Normal file
42
src/phases/PreparePhase.ts
Normal 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
78
src/phases/ZodifyPhase.ts
Normal 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
6
src/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export interface ExtolloCompileConfig {
|
||||
compileDir?: string,
|
||||
zodify?: string[],
|
||||
'non-source': string[],
|
||||
}
|
Loading…
Reference in New Issue
Block a user