Skip to content

Commit 0d3195f

Browse files
committed
feat(loader): basic support for group.write()
1 parent 348800c commit 0d3195f

File tree

6 files changed

+65
-37
lines changed

6 files changed

+65
-37
lines changed

packages/hmr/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class Watcher extends Service {
7272
constructor(ctx: Context, public config: Watcher.Config) {
7373
super(ctx, 'hmr')
7474
this.base = resolve(ctx.baseDir, config.base || '')
75-
this.initialURL = pathToFileURL(ctx.loader.file.name).href
75+
this.initialURL = ctx.loader.file.url
7676
}
7777

7878
relative(filename: string) {

packages/loader/src/entry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Context, ForkScope, Inject } from '@cordisjs/core'
22
import { Dict } from 'cosmokit'
3-
import Loader from './shared.ts'
3+
import { Loader } from './shared.ts'
44
import { EntryGroup } from './group.ts'
55

66
export namespace Entry {
@@ -46,7 +46,7 @@ export class Entry {
4646
static key = Symbol('cordis.entry')
4747

4848
public fork?: ForkScope
49-
public isUpdate = false
49+
public suspend = false
5050
public options!: Entry.Options
5151
public children?: EntryGroup
5252

@@ -150,7 +150,7 @@ export class Entry {
150150
if (!this.loader.isTruthyLike(options.when) || options.disabled) {
151151
this.stop()
152152
} else if (this.fork) {
153-
this.isUpdate = true
153+
this.suspend = true
154154
for (const [key, label] of Object.entries(legacy.isolate ?? {})) {
155155
if (this.options.isolate?.[key] === label) continue
156156
const name = this.resolveRealm(label)

packages/loader/src/file.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { access, constants, readFile, writeFile } from 'node:fs/promises'
22
import { pathToFileURL } from 'node:url'
33
import * as yaml from 'js-yaml'
4-
import Loader from './shared.ts'
4+
import { Entry } from './entry.ts'
5+
import { Loader } from './shared.ts'
56

67
export class FileLoader<T extends Loader = Loader> {
8+
public url: string
79
public suspend = false
810
public mutable = false
911

1012
private _writeTask?: NodeJS.Timeout
1113

12-
constructor(public loader: T, public name: string, public type?: string) {}
14+
constructor(public loader: T, public name: string, public type?: string) {
15+
this.url = pathToFileURL(name).href
16+
}
1317

1418
async checkAccess() {
1519
if (!this.type) return
@@ -19,7 +23,7 @@ export class FileLoader<T extends Loader = Loader> {
1923
} catch {}
2024
}
2125

22-
async read() {
26+
async read(): Promise<Entry.Options[]> {
2327
if (this.type === 'application/yaml') {
2428
return yaml.load(await readFile(this.name, 'utf8')) as any
2529
} else if (this.type === 'application/json') {
@@ -31,7 +35,7 @@ export class FileLoader<T extends Loader = Loader> {
3135
}
3236
}
3337

34-
private async _write(config: any) {
38+
private async _write(config: Entry.Options[]) {
3539
this.suspend = true
3640
if (!this.mutable) {
3741
throw new Error(`cannot overwrite readonly config`)
@@ -43,7 +47,8 @@ export class FileLoader<T extends Loader = Loader> {
4347
}
4448
}
4549

46-
write(config: any) {
50+
write(config: Entry.Options[]) {
51+
this.loader.app.emit('config')
4752
clearTimeout(this._writeTask)
4853
this._writeTask = setTimeout(() => {
4954
this._writeTask = undefined
@@ -53,9 +58,13 @@ export class FileLoader<T extends Loader = Loader> {
5358

5459
async import(name: string) {
5560
if (this.loader.internal) {
56-
return this.loader.internal.import(name, pathToFileURL(this.name).href, {})
61+
return this.loader.internal.import(name, this.url, {})
5762
} else {
5863
return import(name)
5964
}
6065
}
66+
67+
dispose() {
68+
clearTimeout(this._writeTask)
69+
}
6170
}

packages/loader/src/group.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { Context } from '@cordisjs/core'
22
import { FileLoader } from './file.ts'
33
import { Entry } from './entry.ts'
4+
import { fileURLToPath } from 'node:url'
45

56
export class EntryGroup {
67
public data: Entry.Options[] = []
78

8-
constructor(public ctx: Context) {}
9+
constructor(public ctx: Context) {
10+
ctx.on('dispose', () => this.stop())
11+
}
912

1013
async _create(options: Omit<Entry.Options, 'id'>) {
1114
const id = this.ctx.loader.ensureId(options)
@@ -47,7 +50,11 @@ export class EntryGroup {
4750
}
4851
}
4952

50-
dispose() {
53+
write() {
54+
this.ctx.loader.file.write(this.ctx.loader.root.data)
55+
}
56+
57+
stop() {
5158
for (const options of this.data) {
5259
this._remove(options.id)
5360
}
@@ -72,9 +79,6 @@ export function defineGroup(config?: Entry.Options[], options: GroupOptions = {}
7279
constructor(public ctx: Context) {
7380
super(ctx)
7481
ctx.scope.entry!.children = this
75-
ctx.on('dispose', () => {
76-
this.dispose()
77-
})
7882
ctx.accept((config: Entry.Options[]) => {
7983
this.update(config)
8084
}, { passive: true, immediate: true })
@@ -88,8 +92,8 @@ export const group = defineGroup()
8892

8993
export namespace Import {
9094
export interface Config {
91-
path: string
92-
disabled?: boolean
95+
url: string
96+
// disabled?: boolean
9397
}
9498
}
9599

@@ -98,5 +102,19 @@ export class Import extends EntryGroup {
98102

99103
constructor(public ctx: Context, public config: Import.Config) {
100104
super(ctx)
105+
ctx.on('ready', () => this.start())
106+
}
107+
108+
async start() {
109+
const { url } = this.config
110+
const filename = fileURLToPath(new URL(url, this.ctx.loader.file.url))
111+
this.file = new FileLoader(this.ctx.loader, filename)
112+
this.update(await this.file.read())
113+
await this.file.checkAccess()
114+
}
115+
116+
stop() {
117+
this.file?.dispose()
118+
return super.stop()
101119
}
102120
}

packages/loader/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Loader from './shared.ts'
1+
import { Loader } from './shared.ts'
22
import { promises as fs } from 'fs'
33
import * as dotenv from 'dotenv'
44
import * as path from 'path'

packages/loader/src/shared.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export namespace Loader {
5858
}
5959
}
6060

61-
export abstract class Loader<T extends Loader.Options = Loader.Options> extends Service<Entry.Options[]> {
61+
export abstract class Loader<T extends Loader.Options = Loader.Options> extends Service<T> {
6262
// TODO auto inject optional when provided?
6363
static inject = {
6464
optional: ['loader'],
@@ -83,7 +83,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
8383

8484
private tasks = new Set<Promise<any>>()
8585

86-
constructor(public app: Context, public options: T) {
86+
constructor(public app: Context, public config: T) {
8787
super(app, 'loader', true)
8888
this.root = new EntryGroup(this.app)
8989
this.realms['#'] = app.root[Context.isolate]
@@ -99,10 +99,10 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
9999

100100
this.app.on('internal/before-update', (fork, config) => {
101101
if (!fork.entry) return
102-
if (fork.entry.isUpdate) return fork.entry.isUpdate = false
102+
if (fork.entry.suspend) return fork.entry.suspend = false
103103
const { schema } = fork.runtime
104104
fork.entry.options.config = schema ? schema.simplify(config) : config
105-
this.file.write(this.config)
105+
fork.entry.parent.write()
106106
})
107107

108108
this.app.on('internal/fork', (fork) => {
@@ -121,7 +121,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
121121
fork.entry.options.disabled = true
122122
fork.entry.fork = undefined
123123
fork.entry.stop()
124-
this.file.write(this.config)
124+
fork.entry.parent.write()
125125
})
126126
}
127127

@@ -144,29 +144,30 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
144144
} else {
145145
await this.findConfig()
146146
}
147+
this.app.on('dispose', () => this.file.dispose())
147148
await this.file.checkAccess()
148149
this.app.provide('baseDir', this.baseDir, true)
149150
}
150151

151152
private async findConfig() {
153+
const { name, initial } = this.config
152154
const dirents = await readdir(this.baseDir, { withFileTypes: true })
153155
for (const extension of supported) {
154-
const dirent = dirents.find(dirent => dirent.name === this.options.name + extension)
156+
const dirent = dirents.find(dirent => dirent.name === name + extension)
155157
if (!dirent) continue
156158
if (!dirent.isFile()) {
157159
throw new Error(`config file "${dirent.name}" is not a file`)
158160
}
159161
const type = writable[extension]
160-
const name = path.resolve(this.baseDir, this.options.name + extension)
161-
this.file = new FileLoader(this, name, type)
162+
const filename = path.resolve(this.baseDir, name + extension)
163+
this.file = new FileLoader(this, filename, type)
162164
return
163165
}
164-
if (this.options.initial) {
165-
this.config = this.options.initial as any
166+
if (initial) {
166167
const type = writable['.yml']
167-
const name = path.resolve(this.baseDir, this.options.name + '.yml')
168-
this.file = new FileLoader(this, name, type)
169-
return this.file.write(this.config)
168+
const filename = path.resolve(this.baseDir, name + '.yml')
169+
this.file = new FileLoader(this, filename, type)
170+
return this.file.write(initial as any)
170171
}
171172
throw new Error('config file not found')
172173
}
@@ -218,7 +219,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
218219
override[key] = value
219220
}
220221
}
221-
this.file.write(this.config)
222+
entry.parent.write()
222223
return entry.update(override)
223224
}
224225

@@ -231,15 +232,15 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
231232
async create(options: Omit<Entry.Options, 'id'>, parent: string | null = null, position = Infinity) {
232233
const group = this.resolveGroup(parent)
233234
group.data.splice(position, 0, options as Entry.Options)
234-
this.file.write(this.config)
235+
group.write()
235236
return group._create(options)
236237
}
237238

238239
remove(id: string) {
239240
const entry = this.entries[id]
240241
if (!entry) throw new Error(`entry ${id} not found`)
241242
entry.parent._remove(id)
242-
this.file.write(this.config)
243+
entry.parent.write()
243244
}
244245

245246
transfer(id: string, parent: string | null, position = Infinity) {
@@ -249,7 +250,8 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
249250
const target = this.resolveGroup(parent)
250251
source._unlink(entry.options)
251252
target.data.splice(position, 0, entry.options)
252-
this.file.write(this.config)
253+
source.write()
254+
target.write()
253255
if (source === target) return
254256
entry.parent = target
255257
if (!entry.fork) return
@@ -275,8 +277,7 @@ export abstract class Loader<T extends Loader.Options = Loader.Options> extends
275277
}
276278

277279
async start() {
278-
this.config = await this.file.read()
279-
this.root.update(this.config)
280+
this.root.update(await this.file.read())
280281

281282
while (this.tasks.size) {
282283
await Promise.all(this.tasks)

0 commit comments

Comments
 (0)