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