Skip to content

Commit

Permalink
feat(loader): base import loader
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 25, 2024
1 parent c7eeecb commit 5f8cbb5
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 132 deletions.
115 changes: 107 additions & 8 deletions packages/loader/src/file.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { access, constants, readFile, writeFile } from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
import { Context } from '@cordisjs/core'
import { dirname, extname, resolve } from 'node:path'
import { access, constants, readdir, readFile, stat, writeFile } from 'node:fs/promises'
import { fileURLToPath, pathToFileURL } from 'node:url'
import * as yaml from 'js-yaml'
import { Entry } from './entry.ts'
import { EntryGroup } from './group.ts'
import { Loader } from './shared.ts'

export class FileLoader<T extends Loader = Loader> {
export class LoaderFile {
public url: string
public suspend = false
public mutable = false

private _writeTask?: NodeJS.Timeout

constructor(public loader: T, public name: string, public type?: string) {
constructor(public ctx: Context, public name: string, public type?: string) {
this.url = pathToFileURL(name).href
}

Expand Down Expand Up @@ -48,7 +51,7 @@ export class FileLoader<T extends Loader = Loader> {
}

write(config: Entry.Options[]) {
this.loader.ctx.emit('config')
this.ctx.emit('config')
clearTimeout(this._writeTask)
this._writeTask = setTimeout(() => {
this._writeTask = undefined
Expand All @@ -57,8 +60,8 @@ export class FileLoader<T extends Loader = Loader> {
}

async import(name: string) {
if (this.loader.internal) {
return this.loader.internal.import(name, this.url, {})
if (this.ctx.loader.internal) {
return this.ctx.loader.internal.import(name, this.url, {})
} else {
return import(name)
}
Expand All @@ -69,7 +72,7 @@ export class FileLoader<T extends Loader = Loader> {
}
}

export namespace FileLoader {
export namespace LoaderFile {
export const writable = {
'.json': 'application/json',
'.yaml': 'application/yaml',
Expand All @@ -85,3 +88,99 @@ export namespace FileLoader {
}
}
}

export class BaseLoader extends EntryGroup {
public file!: LoaderFile

constructor(public ctx: Context) {
super(ctx)
ctx.on('ready', () => this.start())
}

async start() {
await this.refresh()
await this.file.checkAccess()
}

async refresh() {
this._update(await this.file.read())
}

stop() {
this.file?.dispose()
return super.stop()
}

write() {
return this.file!.write(this.data)
}

async init(baseDir: string, options: Loader.Config) {
if (options.filename) {
const filename = resolve(baseDir, options.filename)
const stats = await stat(filename)
if (stats.isFile()) {
baseDir = dirname(filename)
const ext = extname(filename)
const type = LoaderFile.writable[ext]
if (!LoaderFile.supported.has(ext)) {
throw new Error(`extension "${ext}" not supported`)
}
this.file = new LoaderFile(this.ctx, filename, type)
} else {
baseDir = filename
await this.findConfig(baseDir, options)
}
} else {
await this.findConfig(baseDir, options)
}
this.ctx.provide('baseDir', baseDir, true)
}

private async findConfig(baseDir: string, options: Loader.Config) {
const { name, initial } = options
const dirents = await readdir(baseDir, { withFileTypes: true })
for (const extension of LoaderFile.supported) {
const dirent = dirents.find(dirent => dirent.name === name + extension)
if (!dirent) continue
if (!dirent.isFile()) {
throw new Error(`config file "${dirent.name}" is not a file`)
}
const type = LoaderFile.writable[extension]
const filename = resolve(baseDir, name + extension)
this.file = new LoaderFile(this.ctx, filename, type)
return
}
if (initial) {
const type = LoaderFile.writable['.yml']
const filename = resolve(baseDir, name + '.yml')
this.file = new LoaderFile(this.ctx, filename, type)
return this.file.write(initial as any)
}
throw new Error('config file not found')
}
}

export namespace Import {
export interface Config {
url: string
// disabled?: boolean
}
}

export class Import extends BaseLoader {
constructor(ctx: Context, public config: Import.Config) {
super(ctx)
}

async start() {
const { url } = this.config
const filename = fileURLToPath(new URL(url, this.ctx.loader.file.url))
const ext = extname(filename)
if (!LoaderFile.supported.has(ext)) {
throw new Error(`extension "${ext}" not supported`)
}
this.file = new LoaderFile(this.ctx, filename, LoaderFile.writable[ext])
await super.start()
}
}
61 changes: 1 addition & 60 deletions packages/loader/src/group.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Context } from '@cordisjs/core'
import { FileLoader } from './file.ts'
import { Entry } from './entry.ts'
import { fileURLToPath } from 'node:url'
import { extname } from 'node:path'

export class EntryGroup {
public data: Entry.Options[] = []
Expand Down Expand Up @@ -52,11 +49,7 @@ export class EntryGroup {
}

write() {
if (this.ctx.scope.entry) {
return this.ctx.scope.entry!.parent.write()
} else {
return this.ctx.loader.file.write(this.ctx.loader.data)
}
return this.ctx.scope.entry!.parent.write()
}

stop() {
Expand Down Expand Up @@ -94,55 +87,3 @@ export function defineGroup(config?: Entry.Options[], options: GroupOptions = {}
}

export const group = defineGroup()

export class BaseImportLoader extends EntryGroup {
public file!: FileLoader

constructor(public ctx: Context) {
super(ctx)
ctx.on('ready', () => this.start())
}

async start() {
await this.refresh()
await this.file.checkAccess()
}

async refresh() {
this._update(await this.file.read())
}

stop() {
this.file?.dispose()
return super.stop()
}

write() {
return this.file!.write(this.data)
}
}

export namespace Import {
export interface Config {
url: string
// disabled?: boolean
}
}

export class Import extends BaseImportLoader {
constructor(ctx: Context, public config: Import.Config) {
super(ctx)
}

async start() {
const { url } = this.config
const filename = fileURLToPath(new URL(url, this.ctx.loader.file.url))
const ext = extname(filename)
if (!FileLoader.supported.has(ext)) {
throw new Error(`extension "${ext}" not supported`)
}
this.file = new FileLoader(this.ctx.loader, filename, FileLoader.writable[ext])
this._update(await this.file.read())
await this.file.checkAccess()
}
}
8 changes: 4 additions & 4 deletions packages/loader/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const oldEnv = { ...process.env }
class NodeLoader extends Loader {
static readonly exitCode = 51

async start() {
async init(baseDir: string, options: Loader.Config) {
await super.init(baseDir, options)

// restore process.env
for (const key in process.env) {
if (key in oldEnv) {
Expand All @@ -26,7 +28,7 @@ class NodeLoader extends Loader {
const envFiles = ['.env', '.env.local']
for (const filename of envFiles) {
try {
const raw = await fs.readFile(path.resolve(this.baseDir, filename), 'utf8')
const raw = await fs.readFile(path.resolve(this.ctx.baseDir, filename), 'utf8')
Object.assign(override, dotenv.parse(raw))
} catch {}
}
Expand All @@ -35,8 +37,6 @@ class NodeLoader extends Loader {
for (const key in override) {
process.env[key] = override[key]
}

return await super.start()
}

exit(code = NodeLoader.exitCode) {
Expand Down
54 changes: 4 additions & 50 deletions packages/loader/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { Context, EffectScope } from '@cordisjs/core'
import { Dict, isNullable, valueMap } from 'cosmokit'
import { readdir, stat } from 'node:fs/promises'
import { ModuleLoader } from './internal.ts'
import { interpolate } from './utils.ts'
import { Entry } from './entry.ts'
import { BaseImportLoader } from './group.ts'
import { FileLoader } from './file.ts'
import * as path from 'node:path'
import { BaseLoader } from './file.ts'

export * from './entry.ts'
export * from './file.ts'
Expand All @@ -21,6 +18,7 @@ declare module '@cordisjs/core' {
}

interface Context {
baseDir: string
loader: Loader
}

Expand All @@ -38,20 +36,18 @@ declare module '@cordisjs/core' {
export namespace Loader {
export interface Config {
name: string
immutable?: boolean
initial?: Omit<Entry.Options, 'id'>[]
filename?: string
}
}

export abstract class Loader extends BaseImportLoader {
export abstract class Loader extends BaseLoader {
// TODO auto inject optional when provided?
static inject = {
optional: ['loader'],
}

// process
public baseDir = process.cwd()
public envData = process.env.CORDIS_SHARED
? JSON.parse(process.env.CORDIS_SHARED)
: { startTime: Date.now() }
Expand Down Expand Up @@ -106,55 +102,13 @@ export abstract class Loader extends BaseImportLoader {
}

async start() {
if (this.config.filename) {
const filename = path.resolve(this.baseDir, this.config.filename)
const stats = await stat(filename)
if (stats.isFile()) {
this.baseDir = path.dirname(filename)
const extname = path.extname(filename)
const type = FileLoader.writable[extname]
if (!FileLoader.supported.has(extname)) {
throw new Error(`extension "${extname}" not supported`)
}
this.file = new FileLoader(this, filename, type)
} else {
this.baseDir = filename
await this.findConfig()
}
} else {
await this.findConfig()
}
this.ctx.provide('baseDir', this.baseDir, true)

await this.init(process.cwd(), this.config)
await super.start()
while (this.tasks.size) {
await Promise.all(this.tasks)
}
}

private async findConfig() {
const { name, initial } = this.config
const dirents = await readdir(this.baseDir, { withFileTypes: true })
for (const extension of FileLoader.supported) {
const dirent = dirents.find(dirent => dirent.name === name + extension)
if (!dirent) continue
if (!dirent.isFile()) {
throw new Error(`config file "${dirent.name}" is not a file`)
}
const type = FileLoader.writable[extension]
const filename = path.resolve(this.baseDir, name + extension)
this.file = new FileLoader(this, filename, type)
return
}
if (initial) {
const type = FileLoader.writable['.yml']
const filename = path.resolve(this.baseDir, name + '.yml')
this.file = new FileLoader(this, filename, type)
return this.file.write(initial as any)
}
throw new Error('config file not found')
}

interpolate(source: any) {
if (typeof source === 'string') {
return interpolate(source, this.params, /\$\{\{(.+?)\}\}/g)
Expand Down
6 changes: 3 additions & 3 deletions packages/loader/tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai'
import { Context } from '@cordisjs/core'
import MockLoader from './utils'

describe('loader: basic', () => {
describe('loader: basic support', () => {
const root = new Context()
root.plugin(MockLoader)
const loader = root.loader as MockLoader
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('loader: basic', () => {
disabled: true,
}],
}])
await loader.start()
await loader.refresh()

loader.expectEnable(foo, {})
loader.expectEnable(bar, { a: 1 })
Expand All @@ -52,7 +52,7 @@ describe('loader: basic', () => {
id: '4',
name: 'qux',
}])
await loader.start()
await loader.refresh()

loader.expectEnable(foo, {})
loader.expectDisable(bar)
Expand Down
Loading

0 comments on commit 5f8cbb5

Please sign in to comment.