Skip to content

Commit

Permalink
refa: refactor import config
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Aug 14, 2024
1 parent 2b007fe commit 81e98a7
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 239 deletions.
6 changes: 3 additions & 3 deletions packages/cordis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
"types": "./lib/plugins/group.d.ts",
"default": "./lib/plugins/group.mjs"
},
"./import-config": {
"types": "./lib/plugins/import-config.d.ts",
"default": "./lib/plugins/import-config.mjs"
"./import": {
"types": "./lib/plugins/import.d.ts",
"default": "./lib/plugins/import.mjs"
},
"./loader": {
"types": "./lib/plugins/loader.d.ts",
Expand Down
3 changes: 0 additions & 3 deletions packages/cordis/src/plugins/import-config.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/cordis/src/plugins/import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Import } from '@cordisjs/loader'

export default Import
10 changes: 5 additions & 5 deletions packages/hmr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ class Watcher extends Service {

this.watcher.on('change', async (path) => {
this.ctx.logger.debug('change detected:', path)
const filename = pathToFileURL(resolve(this.base, path)).href
if (this.externals.has(filename)) return loader.exit()
const url = pathToFileURL(resolve(this.base, path)).href
if (this.externals.has(url)) return loader.exit()

if (loader.internal!.loadCache.has(filename)) {
this.stashed.add(filename)
if (loader.internal!.loadCache.has(url)) {
this.stashed.add(url)
return triggerLocalReload()
}

const file = this.ctx.loader.files[filename]
const file = this.ctx.loader.files[url]
if (!file) return
if (file.suspend) {
file.suspend = false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Context, ForkScope } from '@cordisjs/core'
import { isNullable } from 'cosmokit'
import { Loader } from './loader.ts'
import { Loader } from '../loader.ts'
import { EntryGroup } from './group.ts'
import { EntryTree } from './tree.ts'
import { interpolate } from './utils.ts'

export interface EntryOptions {
id: string
Expand Down Expand Up @@ -75,6 +76,11 @@ export class Entry<C extends Context = Context> {
return !this.parent.ctx.bail('loader/entry-check', this)
}

_resolveConfig(plugin: any) {
if (!plugin[EntryGroup.key]) return this.options.config
return interpolate({}, this.options.config) // FIXME
}

patch(options: Partial<EntryOptions> = {}) {
// step 1: prepare isolate map
const meta = {} as EntryUpdateMeta
Expand All @@ -86,7 +92,7 @@ export class Entry<C extends Context = Context> {
if (this.fork && 'config' in options) {
// step 2: update fork (when options.config is updated)
this.suspend = true
this.fork.update(this.options.config)
this.fork.update(this._resolveConfig(this.fork.runtime.plugin))
} else if (this.subgroup && 'disabled' in options) {
// step 3: check children (when options.disabled is updated)
const tree = this.subtree ?? this.parent.tree
Expand Down Expand Up @@ -146,7 +152,7 @@ export class Entry<C extends Context = Context> {
const plugin = this.loader.unwrapExports(exports)
this.patch()
this.ctx[Entry.key] = this
this.fork = this.ctx.plugin(plugin, this.options.config)
this.fork = this.ctx.plugin(plugin, this._resolveConfig(plugin))
this.context.emit('loader/entry-fork', this, 'apply')
}

Expand Down
99 changes: 99 additions & 0 deletions packages/loader/src/config/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { access, constants, readFile, rename, writeFile } from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
import { remove } from 'cosmokit'
import * as yaml from 'js-yaml'
import { EntryOptions } from './entry.ts'
import { Loader } from '../loader.ts'
import { JsExpr } from './utils.ts'

export const schema = yaml.JSON_SCHEMA.extend(JsExpr)

export class LoaderFile {
public suspend = false
public readonly: boolean
public refs: FileRef[] = []
public writeTask?: NodeJS.Timeout

constructor(public loader: Loader, public name: string, public type?: string) {
this.readonly = !type
}

async checkAccess() {
if (!this.type) return
try {
await access(this.name, constants.W_OK)
} catch {
this.readonly = true
}
}

async read(): Promise<EntryOptions[]> {
if (this.type === 'application/yaml') {
return yaml.load(await readFile(this.name, 'utf8'), { schema }) as any
} else if (this.type === 'application/json') {
// we do not use require / import here because it will pollute cache
return JSON.parse(await readFile(this.name, 'utf8')) as any
} else {
const module = await import(this.name)
return module.default || module
}
}

private async _write(config: EntryOptions[]) {
this.suspend = true
if (this.readonly) {
throw new Error(`cannot overwrite readonly config`)
}
if (this.type === 'application/yaml') {
await writeFile(this.name + '.tmp', yaml.dump(config, { schema }))
} else if (this.type === 'application/json') {
await writeFile(this.name + '.tmp', JSON.stringify(config, null, 2))
}
await rename(this.name + '.tmp', this.name)
}

write(config: EntryOptions[]) {
clearTimeout(this.writeTask)
this.writeTask = setTimeout(() => {
this.writeTask = undefined
this._write(config)
}, 0)
}

ref() {
return new FileRef(this)
}
}

export namespace LoaderFile {
export const writable = {
'.json': 'application/json',
'.yaml': 'application/yaml',
'.yml': 'application/yaml',
}

export const supported = new Set(Object.keys(writable))

if (typeof require !== 'undefined') {
// eslint-disable-next-line n/no-deprecated-api
for (const extname in require.extensions) {
supported.add(extname)
}
}
}

export class FileRef<F extends LoaderFile = LoaderFile> {
public url: string

constructor(public file: F) {
this.file.refs.push(this)
this.url = pathToFileURL(file.name).href
file.loader.files[this.url] ??= this.file
}

stop() {
remove(this.file.refs, this)
if (this.file.refs.length) return
delete this.file.loader.files[this.url]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export class Group extends EntryGroup {
static initial: Omit<EntryOptions, 'id'>[] = []
static readonly [EntryGroup.key] = true

// TODO support options
constructor(public ctx: Context) {
super(ctx, ctx.scope.entry!.parent.tree)
ctx.on('dispose', () => this.stop())
Expand Down
107 changes: 107 additions & 0 deletions packages/loader/src/config/import.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Context } from '@cordisjs/core'
import { dirname, extname, resolve } from 'node:path'
import { readdir, stat } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { EntryTree } from './tree.ts'
import { FileRef, LoaderFile } from './file.ts'
import Loader from '../loader.ts'

export class ImportTree<C extends Context = Context> extends EntryTree<C> {
static reusable = true

public ref!: FileRef

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

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

async refresh() {
this.root.update(await this.ref.file.read())
}

stop() {
this.ref?.stop()
return this.root.stop()
}

write() {
this.context.emit('loader/config-update')
return this.ref.file.write(this.root.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.ref = new LoaderFile(this.ctx.loader, filename, type).ref()
} else {
baseDir = filename
await this._init(baseDir, options)
}
} else {
await this._init(baseDir, options)
}
this.ctx.provide('baseDir', baseDir, true)
this.url = this.ref.url
}

private async _init(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.ref = new LoaderFile(this.ctx.loader, filename, type).ref()
return
}
if (initial) {
const type = LoaderFile.writable['.yml']
const filename = resolve(baseDir, name + '.yml')
this.ref = new LoaderFile(this.ctx.loader, filename, type).ref()
return this.ref.file.write(initial as any)
}
throw new Error('config file not found')
}
}

export namespace Import {
export interface Config {
url: string
}
}

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

async start() {
const { url } = this.config
const filename = fileURLToPath(new URL(url, this.ctx.scope.entry!.parent.tree.url))
const ext = extname(filename)
if (!LoaderFile.supported.has(ext)) {
throw new Error(`extension "${ext}" not supported`)
}
this.ref = new LoaderFile(this.ctx.loader, filename, LoaderFile.writable[ext]).ref()
await super.start()
}
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { EntryGroup } from './group.ts'

export abstract class EntryTree<C extends Context = Context> {
static readonly sep = ':'
static readonly [EntryGroup.key] = true

public url!: string
public root: EntryGroup
Expand Down
File renamed without changes.
Loading

0 comments on commit 81e98a7

Please sign in to comment.