paralayer 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -7
- package/src/bin.ts +32 -0
- package/src/index.ts +9 -0
- package/src/paralayer.ts +268 -0
- package/bin.js +0 -3
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +0 -25
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -7
- package/dist/paralayer.d.ts +0 -42
- package/dist/paralayer.js +0 -220
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paralayer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "imkost",
|
|
@@ -16,20 +16,18 @@
|
|
|
16
16
|
"release": "sh -c 'npm version ${1:-patch} && npm run build && npm publish' --"
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
|
-
"paralayer": "bin.
|
|
19
|
+
"paralayer": "./src/bin.ts"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
23
|
-
"
|
|
24
|
-
"types": "./dist/index.d.ts"
|
|
23
|
+
"import": "./src/index.ts"
|
|
25
24
|
}
|
|
26
25
|
},
|
|
27
26
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"bin.js"
|
|
27
|
+
"src"
|
|
30
28
|
],
|
|
31
29
|
"dependencies": {
|
|
32
|
-
"@eposlabs/utils": "^1.0.
|
|
30
|
+
"@eposlabs/utils": "^1.0.13",
|
|
33
31
|
"chokidar": "^4.0.3",
|
|
34
32
|
"minimist": "^1.2.8"
|
|
35
33
|
},
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import minimist from 'minimist'
|
|
4
|
+
import { paralayer } from './index.ts'
|
|
5
|
+
|
|
6
|
+
const argv = minimist(process.argv.slice(2), {
|
|
7
|
+
string: ['default'],
|
|
8
|
+
boolean: ['watch', 'globalize'],
|
|
9
|
+
default: { watch: false, default: null, globalize: false },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const paths = argv._
|
|
13
|
+
const input = paths.slice(0, -1)
|
|
14
|
+
const output = paths.at(-1)
|
|
15
|
+
|
|
16
|
+
if (input.length === 0) {
|
|
17
|
+
console.error('[paralayer] Input directory is not provided')
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!output) {
|
|
22
|
+
console.error('[paralayer] Output directory is not provided')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await paralayer({
|
|
27
|
+
input: input,
|
|
28
|
+
output: output,
|
|
29
|
+
watch: argv.watch,
|
|
30
|
+
default: argv.default,
|
|
31
|
+
globalize: argv.globalize,
|
|
32
|
+
})
|
package/src/index.ts
ADDED
package/src/paralayer.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Queue, safe, Unit } from '@eposlabs/utils'
|
|
2
|
+
import type { FSWatcher } from 'chokidar'
|
|
3
|
+
import { watch } from 'chokidar'
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { basename, extname, join, relative } from 'node:path'
|
|
6
|
+
|
|
7
|
+
export type DirPath = string
|
|
8
|
+
|
|
9
|
+
export type File = {
|
|
10
|
+
content: string
|
|
11
|
+
names: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Options = {
|
|
15
|
+
input: DirPath | DirPath[]
|
|
16
|
+
output: DirPath
|
|
17
|
+
watch?: boolean
|
|
18
|
+
/** Default layer name. If a file name does not have layer tags, default name will be used. */
|
|
19
|
+
default?: string | null
|
|
20
|
+
/** Whether the layer variables should be exposed globally. */
|
|
21
|
+
globalize?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Paralayer extends Unit {
|
|
25
|
+
private files: { [path: string]: File | null } = {}
|
|
26
|
+
private options: Options
|
|
27
|
+
private started = false
|
|
28
|
+
private ready = false
|
|
29
|
+
private ready$ = Promise.withResolvers<void>()
|
|
30
|
+
private queue = new Queue()
|
|
31
|
+
private extensions = new Set(['.ts', '.tsx', '.js', '.jsx'])
|
|
32
|
+
private previousLayers: string[] = []
|
|
33
|
+
private watcher: FSWatcher | null = null
|
|
34
|
+
|
|
35
|
+
constructor(options: Options) {
|
|
36
|
+
super()
|
|
37
|
+
this.options = options
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start = async () => {
|
|
41
|
+
if (this.started) return
|
|
42
|
+
this.started = true
|
|
43
|
+
|
|
44
|
+
await safe(rm(this.options.output, { recursive: true }))
|
|
45
|
+
|
|
46
|
+
this.watcher = watch(this.options.input)
|
|
47
|
+
this.watcher.on('all', this.onAll)
|
|
48
|
+
this.watcher.on('ready', this.onReady)
|
|
49
|
+
|
|
50
|
+
await this.ready$.promise
|
|
51
|
+
await this.queue.run(() => this.build())
|
|
52
|
+
if (!this.options.watch) await this.watcher.close()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async readSetupJs() {
|
|
56
|
+
const setupJsPath = join(this.options.output, 'setup.js')
|
|
57
|
+
return await readFile(setupJsPath, 'utf-8')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// HANDLERS
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
private onAll = async (event: string, path: string) => {
|
|
65
|
+
// Process only file events
|
|
66
|
+
if (!['add', 'change', 'unlink'].includes(event)) return
|
|
67
|
+
|
|
68
|
+
// Not supported file extension? -> Ignore
|
|
69
|
+
const ext = extname(path)
|
|
70
|
+
if (!this.extensions.has(ext)) return
|
|
71
|
+
|
|
72
|
+
// No layer in the file name? -> Ignore
|
|
73
|
+
if (!this.getLayer(path)) return
|
|
74
|
+
|
|
75
|
+
// Initial scan? -> Just register file
|
|
76
|
+
if (!this.ready) {
|
|
77
|
+
this.files[path] = null
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// File removed? -> Remove and rebuild
|
|
82
|
+
if (event === 'unlink') {
|
|
83
|
+
delete this.files[path]
|
|
84
|
+
await this.queue.run(() => this.build())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// File added/changed? -> Reset its content and rebuild
|
|
88
|
+
if (event === 'add' || event === 'change') {
|
|
89
|
+
this.files[path] = null
|
|
90
|
+
await this.queue.run(() => this.build())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private onReady = async () => {
|
|
95
|
+
this.ready = true
|
|
96
|
+
this.ready$.resolve()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// BUILD
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
private async build() {
|
|
104
|
+
// Group file paths by layers
|
|
105
|
+
const paths = Object.keys(this.files)
|
|
106
|
+
if (paths.length === 0) return
|
|
107
|
+
const pathsByLayers = Object.groupBy(paths, path => this.getLayer(path))
|
|
108
|
+
const allLayers = Object.keys(pathsByLayers)
|
|
109
|
+
|
|
110
|
+
// Ensure output directory exists
|
|
111
|
+
await mkdir(this.options.output, { recursive: true })
|
|
112
|
+
|
|
113
|
+
// Ensure all files are read
|
|
114
|
+
await Promise.all(
|
|
115
|
+
paths.map(async path => {
|
|
116
|
+
if (this.files[path]) return
|
|
117
|
+
const content = await readFile(path, 'utf-8')
|
|
118
|
+
const names = this.extractExportedClassNames(content)
|
|
119
|
+
this.files[path] = { content, names }
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Create layer files
|
|
124
|
+
for (const layer in pathsByLayers) {
|
|
125
|
+
// Generate layer.[layer].ts
|
|
126
|
+
const layerFile = join(this.options.output, `layer.${layer}.ts`)
|
|
127
|
+
const layerPaths = pathsByLayers[layer]!.toSorted()
|
|
128
|
+
const layerContent = this.generateLayerContent(layer, layerPaths)
|
|
129
|
+
await this.write(layerFile, layerContent)
|
|
130
|
+
|
|
131
|
+
// Top layer? -> Generate index.[layer].ts
|
|
132
|
+
if (this.isTopLayer(layer)) {
|
|
133
|
+
const indexFile = join(this.options.output, `index.${layer}.ts`)
|
|
134
|
+
const indexContent = this.generateIndexContent(layer, allLayers)
|
|
135
|
+
await this.write(indexFile, indexContent)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Determine removed layers
|
|
140
|
+
const removedLayers = this.previousLayers.filter(l => !allLayers.includes(l))
|
|
141
|
+
this.previousLayers = allLayers
|
|
142
|
+
|
|
143
|
+
// Delete files of removed layers
|
|
144
|
+
for (const layer of removedLayers) {
|
|
145
|
+
// Delete layer file
|
|
146
|
+
const layerFile = join(this.options.output, `layer.${layer}.ts`)
|
|
147
|
+
await rm(layerFile)
|
|
148
|
+
|
|
149
|
+
// Top layer? -> Remove index file
|
|
150
|
+
if (this.isTopLayer(layer)) {
|
|
151
|
+
const indexFile = join(this.options.output, `index.${layer}.ts`)
|
|
152
|
+
await rm(indexFile)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generate setup.js file
|
|
157
|
+
const setupFile = join(this.options.output, 'setup.js')
|
|
158
|
+
const setupContent = this.generateSetupContent(allLayers)
|
|
159
|
+
await this.write(setupFile, setupContent)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// HELPERS
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
private extractExportedClassNames(content: string) {
|
|
167
|
+
if (content.includes('paralayer-ignore')) return []
|
|
168
|
+
return content
|
|
169
|
+
.split('export class ')
|
|
170
|
+
.slice(1)
|
|
171
|
+
.map(part => part.split(' ')[0].split('<')[0])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private generateLayerContent(layer: string, layerPaths: string[]) {
|
|
175
|
+
const $LayerName = this.getLayerName(layer, '$Pascal')
|
|
176
|
+
const $layerName = this.getLayerName(layer, '$camel')
|
|
177
|
+
const allNames = layerPaths.flatMap(path => this.files[path]?.names ?? [])
|
|
178
|
+
|
|
179
|
+
const imports = layerPaths
|
|
180
|
+
.map(path => {
|
|
181
|
+
const file = this.files[path]
|
|
182
|
+
if (!file) return ''
|
|
183
|
+
if (file.names.length === 0) return ''
|
|
184
|
+
const names = file.names
|
|
185
|
+
const types = file.names.map(name => `type ${name} as ${name}Type`)
|
|
186
|
+
const relativePath = relative(this.options.output, path)
|
|
187
|
+
return `import { ${[...names, ...types].join(', ')} } from '${relativePath}'`
|
|
188
|
+
})
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
|
|
191
|
+
const assign = [`Object.assign(${$layerName}, {`, ...allNames.map(name => ` ${name},`), `})`]
|
|
192
|
+
|
|
193
|
+
const globals = [
|
|
194
|
+
`declare global {`,
|
|
195
|
+
` var ${$layerName}: ${$LayerName}`,
|
|
196
|
+
``,
|
|
197
|
+
` interface ${$LayerName} {`,
|
|
198
|
+
...allNames.map(name => ` ${name}: typeof ${name}`),
|
|
199
|
+
` }`,
|
|
200
|
+
``,
|
|
201
|
+
` namespace ${$layerName} {`,
|
|
202
|
+
...allNames.map(name => ` export type ${name} = ${name}Type`),
|
|
203
|
+
` }`,
|
|
204
|
+
`}`,
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
return [...imports, '', ...assign, '', ...globals, ''].join('\n')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private generateIndexContent(topLayer: string, allLayers: string[]) {
|
|
211
|
+
const imports = allLayers
|
|
212
|
+
.filter(layer => layer.includes(topLayer))
|
|
213
|
+
.sort((layer1, layer2) => {
|
|
214
|
+
if (layer1.length !== layer2.length) return layer2.length - layer1.length
|
|
215
|
+
return layer1.localeCompare(layer2)
|
|
216
|
+
})
|
|
217
|
+
.map(layer => `import './layer.${layer}.ts'`)
|
|
218
|
+
|
|
219
|
+
return [...imports].join('\n')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private generateSetupContent(allLayers: string[]) {
|
|
223
|
+
const layers = allLayers.toSorted((layer1, layer2) => {
|
|
224
|
+
if (layer1.length !== layer2.length) return layer1.length - layer2.length
|
|
225
|
+
return layer1.localeCompare(layer2)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const vars = layers.map(layer => {
|
|
229
|
+
const $layerName = this.getLayerName(layer, '$camel')
|
|
230
|
+
if (this.options.globalize) return `globalThis.${$layerName} = {}`
|
|
231
|
+
return `const ${$layerName} = {}`
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return [...vars].join('\n')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private getLayer(path: string) {
|
|
238
|
+
const name = basename(path)
|
|
239
|
+
const layer = name.split('.').slice(1, -1).sort().join('.')
|
|
240
|
+
if (layer) return layer
|
|
241
|
+
return this.options.default ?? ''
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getLayerName(layer: string, style: '$camel' | '$Pascal') {
|
|
245
|
+
const LayerName = layer.split('.').map(this.capitalize).join('')
|
|
246
|
+
if (style === '$camel') return `$${this.decapitalize(LayerName)}`
|
|
247
|
+
if (style === '$Pascal') return `$${LayerName}`
|
|
248
|
+
throw this.never
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private capitalize(string: string) {
|
|
252
|
+
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private decapitalize(string: string) {
|
|
256
|
+
return string.charAt(0).toLowerCase() + string.slice(1)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private isTopLayer(layer: string) {
|
|
260
|
+
return !layer.includes('.')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async write(path: string, content: string) {
|
|
264
|
+
const [prevContent] = await safe(() => readFile(path, 'utf-8'))
|
|
265
|
+
if (content === prevContent) return
|
|
266
|
+
await writeFile(path, content, 'utf-8')
|
|
267
|
+
}
|
|
268
|
+
}
|
package/bin.js
DELETED
package/dist/bin.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/bin.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import minimist from 'minimist';
|
|
2
|
-
import { paralayer } from './index.js';
|
|
3
|
-
const argv = minimist(process.argv.slice(2), {
|
|
4
|
-
string: ['default'],
|
|
5
|
-
boolean: ['watch', 'globalize'],
|
|
6
|
-
default: { watch: false, default: null, globalize: false },
|
|
7
|
-
});
|
|
8
|
-
const paths = argv._;
|
|
9
|
-
const input = paths.slice(0, -1);
|
|
10
|
-
const output = paths.at(-1);
|
|
11
|
-
if (input.length === 0) {
|
|
12
|
-
console.error('[paralayer] Input directory is not provided');
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
if (!output) {
|
|
16
|
-
console.error('[paralayer] Output directory is not provided');
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
await paralayer({
|
|
20
|
-
input: input,
|
|
21
|
-
output: output,
|
|
22
|
-
watch: argv.watch,
|
|
23
|
-
default: argv.default,
|
|
24
|
-
globalize: argv.globalize,
|
|
25
|
-
});
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
package/dist/paralayer.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import * as $utils from '@eposlabs/utils';
|
|
2
|
-
export type DirPath = string;
|
|
3
|
-
export type File = {
|
|
4
|
-
content: string;
|
|
5
|
-
names: string[];
|
|
6
|
-
};
|
|
7
|
-
export type Options = {
|
|
8
|
-
input: DirPath | DirPath[];
|
|
9
|
-
output: DirPath;
|
|
10
|
-
watch?: boolean;
|
|
11
|
-
/** Default layer name. If a file name does not have layer tags, default name will be used. */
|
|
12
|
-
default?: string | null;
|
|
13
|
-
/** Whether the layer variables should be exposed globally. */
|
|
14
|
-
globalize?: boolean;
|
|
15
|
-
};
|
|
16
|
-
export declare class Paralayer extends $utils.Unit {
|
|
17
|
-
private files;
|
|
18
|
-
private options;
|
|
19
|
-
private started;
|
|
20
|
-
private ready;
|
|
21
|
-
private ready$;
|
|
22
|
-
private queue;
|
|
23
|
-
private extensions;
|
|
24
|
-
private previousLayers;
|
|
25
|
-
private watcher;
|
|
26
|
-
constructor(options: Options);
|
|
27
|
-
start: () => Promise<void>;
|
|
28
|
-
readSetupJs(): Promise<string>;
|
|
29
|
-
private onAll;
|
|
30
|
-
private onReady;
|
|
31
|
-
private build;
|
|
32
|
-
private extractExportedClassNames;
|
|
33
|
-
private generateLayerContent;
|
|
34
|
-
private generateIndexContent;
|
|
35
|
-
private generateSetupContent;
|
|
36
|
-
private getLayer;
|
|
37
|
-
private getLayerName;
|
|
38
|
-
private capitalize;
|
|
39
|
-
private decapitalize;
|
|
40
|
-
private isTopLayer;
|
|
41
|
-
private write;
|
|
42
|
-
}
|
package/dist/paralayer.js
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import * as $chokidar from 'chokidar';
|
|
2
|
-
import * as $fs from 'node:fs/promises';
|
|
3
|
-
import * as $path from 'node:path';
|
|
4
|
-
import * as $utils from '@eposlabs/utils';
|
|
5
|
-
export class Paralayer extends $utils.Unit {
|
|
6
|
-
files = {};
|
|
7
|
-
options;
|
|
8
|
-
started = false;
|
|
9
|
-
ready = false;
|
|
10
|
-
ready$ = Promise.withResolvers();
|
|
11
|
-
queue = new $utils.Queue();
|
|
12
|
-
extensions = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
13
|
-
previousLayers = [];
|
|
14
|
-
watcher = null;
|
|
15
|
-
constructor(options) {
|
|
16
|
-
super();
|
|
17
|
-
this.options = options;
|
|
18
|
-
}
|
|
19
|
-
start = async () => {
|
|
20
|
-
if (this.started)
|
|
21
|
-
return;
|
|
22
|
-
this.started = true;
|
|
23
|
-
await $utils.safe($fs.rm(this.options.output, { recursive: true }));
|
|
24
|
-
this.watcher = $chokidar.watch(this.options.input);
|
|
25
|
-
this.watcher.on('all', this.onAll);
|
|
26
|
-
this.watcher.on('ready', this.onReady);
|
|
27
|
-
await this.ready$.promise;
|
|
28
|
-
await this.queue.run(() => this.build());
|
|
29
|
-
if (!this.options.watch)
|
|
30
|
-
await this.watcher.close();
|
|
31
|
-
};
|
|
32
|
-
async readSetupJs() {
|
|
33
|
-
const setupJsPath = $path.join(this.options.output, 'setup.js');
|
|
34
|
-
return await $fs.readFile(setupJsPath, 'utf-8');
|
|
35
|
-
}
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// HANDLERS
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
onAll = async (event, path) => {
|
|
40
|
-
// Process only file events
|
|
41
|
-
if (!['add', 'change', 'unlink'].includes(event))
|
|
42
|
-
return;
|
|
43
|
-
// Not supported file extension? -> Ignore
|
|
44
|
-
const ext = $path.extname(path);
|
|
45
|
-
if (!this.extensions.has(ext))
|
|
46
|
-
return;
|
|
47
|
-
// No layer in the file name? -> Ignore
|
|
48
|
-
if (!this.getLayer(path))
|
|
49
|
-
return;
|
|
50
|
-
// Initial scan? -> Just register file
|
|
51
|
-
if (!this.ready) {
|
|
52
|
-
this.files[path] = null;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
// File removed? -> Remove and rebuild
|
|
56
|
-
if (event === 'unlink') {
|
|
57
|
-
delete this.files[path];
|
|
58
|
-
await this.queue.run(() => this.build());
|
|
59
|
-
}
|
|
60
|
-
// File added/changed? -> Reset its content and rebuild
|
|
61
|
-
if (event === 'add' || event === 'change') {
|
|
62
|
-
this.files[path] = null;
|
|
63
|
-
await this.queue.run(() => this.build());
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
onReady = async () => {
|
|
67
|
-
this.ready = true;
|
|
68
|
-
this.ready$.resolve();
|
|
69
|
-
};
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// BUILD
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
async build() {
|
|
74
|
-
// Group file paths by layers
|
|
75
|
-
const paths = Object.keys(this.files);
|
|
76
|
-
if (paths.length === 0)
|
|
77
|
-
return;
|
|
78
|
-
const pathsByLayers = Object.groupBy(paths, path => this.getLayer(path));
|
|
79
|
-
const allLayers = Object.keys(pathsByLayers);
|
|
80
|
-
// Ensure output directory exists
|
|
81
|
-
await $fs.mkdir(this.options.output, { recursive: true });
|
|
82
|
-
// Ensure all files are read
|
|
83
|
-
await Promise.all(paths.map(async (path) => {
|
|
84
|
-
if (this.files[path])
|
|
85
|
-
return;
|
|
86
|
-
const content = await $fs.readFile(path, 'utf-8');
|
|
87
|
-
const names = this.extractExportedClassNames(content);
|
|
88
|
-
this.files[path] = { content, names };
|
|
89
|
-
}));
|
|
90
|
-
// Create layer files
|
|
91
|
-
for (const layer in pathsByLayers) {
|
|
92
|
-
// Generate layer.[layer].ts
|
|
93
|
-
const layerFile = $path.join(this.options.output, `layer.${layer}.ts`);
|
|
94
|
-
const layerPaths = pathsByLayers[layer].toSorted();
|
|
95
|
-
const layerContent = this.generateLayerContent(layer, layerPaths);
|
|
96
|
-
await this.write(layerFile, layerContent);
|
|
97
|
-
// Top layer? -> Generate index.[layer].ts
|
|
98
|
-
if (this.isTopLayer(layer)) {
|
|
99
|
-
const indexFile = $path.join(this.options.output, `index.${layer}.ts`);
|
|
100
|
-
const indexContent = this.generateIndexContent(layer, allLayers);
|
|
101
|
-
await this.write(indexFile, indexContent);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// Determine removed layers
|
|
105
|
-
const removedLayers = this.previousLayers.filter(l => !allLayers.includes(l));
|
|
106
|
-
this.previousLayers = allLayers;
|
|
107
|
-
// Delete files of removed layers
|
|
108
|
-
for (const layer of removedLayers) {
|
|
109
|
-
// Delete layer file
|
|
110
|
-
const layerFile = $path.join(this.options.output, `layer.${layer}.ts`);
|
|
111
|
-
await $fs.rm(layerFile);
|
|
112
|
-
// Top layer? -> Remove index file
|
|
113
|
-
if (this.isTopLayer(layer)) {
|
|
114
|
-
const indexFile = $path.join(this.options.output, `index.${layer}.ts`);
|
|
115
|
-
await $fs.rm(indexFile);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Generate setup.js file
|
|
119
|
-
const setupFile = $path.join(this.options.output, 'setup.js');
|
|
120
|
-
const setupContent = this.generateSetupContent(allLayers);
|
|
121
|
-
await this.write(setupFile, setupContent);
|
|
122
|
-
}
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// HELPERS
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
extractExportedClassNames(content) {
|
|
127
|
-
return content
|
|
128
|
-
.split('export class ')
|
|
129
|
-
.slice(1)
|
|
130
|
-
.map(part => part.split(' ')[0].split('<')[0]);
|
|
131
|
-
}
|
|
132
|
-
generateLayerContent(layer, layerPaths) {
|
|
133
|
-
const $LayerName = this.getLayerName(layer, '$Pascal');
|
|
134
|
-
const $layerName = this.getLayerName(layer, '$camel');
|
|
135
|
-
const allNames = layerPaths.flatMap(path => this.files[path]?.names ?? []);
|
|
136
|
-
const imports = layerPaths
|
|
137
|
-
.map(path => {
|
|
138
|
-
const file = this.files[path];
|
|
139
|
-
if (!file)
|
|
140
|
-
return '';
|
|
141
|
-
if (file.names.length === 0)
|
|
142
|
-
return '';
|
|
143
|
-
const names = file.names;
|
|
144
|
-
const types = file.names.map(name => `type ${name} as ${name}Type`);
|
|
145
|
-
const relativePath = $path.relative(this.options.output, path);
|
|
146
|
-
return `import { ${[...names, ...types].join(', ')} } from '${relativePath}'`;
|
|
147
|
-
})
|
|
148
|
-
.filter(Boolean);
|
|
149
|
-
const assign = [`Object.assign(${$layerName}, {`, ...allNames.map(name => ` ${name},`), `})`];
|
|
150
|
-
const globals = [
|
|
151
|
-
`declare global {`,
|
|
152
|
-
` var ${$layerName}: ${$LayerName}`,
|
|
153
|
-
``,
|
|
154
|
-
` interface ${$LayerName} {`,
|
|
155
|
-
...allNames.map(name => ` ${name}: typeof ${name}`),
|
|
156
|
-
` }`,
|
|
157
|
-
``,
|
|
158
|
-
` namespace ${$layerName} {`,
|
|
159
|
-
...allNames.map(name => ` export type ${name} = ${name}Type`),
|
|
160
|
-
` }`,
|
|
161
|
-
`}`,
|
|
162
|
-
];
|
|
163
|
-
return [...imports, '', ...assign, '', ...globals, ''].join('\n');
|
|
164
|
-
}
|
|
165
|
-
generateIndexContent(topLayer, allLayers) {
|
|
166
|
-
const imports = allLayers
|
|
167
|
-
.filter(layer => layer.includes(topLayer))
|
|
168
|
-
.sort((layer1, layer2) => {
|
|
169
|
-
if (layer1.length !== layer2.length)
|
|
170
|
-
return layer2.length - layer1.length;
|
|
171
|
-
return layer1.localeCompare(layer2);
|
|
172
|
-
})
|
|
173
|
-
.map(layer => `import './layer.${layer}.ts'`);
|
|
174
|
-
return [...imports].join('\n');
|
|
175
|
-
}
|
|
176
|
-
generateSetupContent(allLayers) {
|
|
177
|
-
const layers = allLayers.toSorted((layer1, layer2) => {
|
|
178
|
-
if (layer1.length !== layer2.length)
|
|
179
|
-
return layer1.length - layer2.length;
|
|
180
|
-
return layer1.localeCompare(layer2);
|
|
181
|
-
});
|
|
182
|
-
const vars = layers.map(layer => {
|
|
183
|
-
const $layerName = this.getLayerName(layer, '$camel');
|
|
184
|
-
if (this.options.globalize)
|
|
185
|
-
return `globalThis.${$layerName} = {}`;
|
|
186
|
-
return `const ${$layerName} = {}`;
|
|
187
|
-
});
|
|
188
|
-
return [...vars].join('\n');
|
|
189
|
-
}
|
|
190
|
-
getLayer(path) {
|
|
191
|
-
const name = $path.basename(path);
|
|
192
|
-
const layer = name.split('.').slice(1, -1).sort().join('.');
|
|
193
|
-
if (layer)
|
|
194
|
-
return layer;
|
|
195
|
-
return this.options.default ?? '';
|
|
196
|
-
}
|
|
197
|
-
getLayerName(layer, style) {
|
|
198
|
-
const LayerName = layer.split('.').map(this.capitalize).join('');
|
|
199
|
-
if (style === '$camel')
|
|
200
|
-
return `$${this.decapitalize(LayerName)}`;
|
|
201
|
-
if (style === '$Pascal')
|
|
202
|
-
return `$${LayerName}`;
|
|
203
|
-
throw this.never;
|
|
204
|
-
}
|
|
205
|
-
capitalize(string) {
|
|
206
|
-
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
207
|
-
}
|
|
208
|
-
decapitalize(string) {
|
|
209
|
-
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
210
|
-
}
|
|
211
|
-
isTopLayer(layer) {
|
|
212
|
-
return !layer.includes('.');
|
|
213
|
-
}
|
|
214
|
-
async write(path, content) {
|
|
215
|
-
const [prevContent] = await $utils.safe(() => $fs.readFile(path, 'utf-8'));
|
|
216
|
-
if (content === prevContent)
|
|
217
|
-
return;
|
|
218
|
-
await $fs.writeFile(path, content, 'utf-8');
|
|
219
|
-
}
|
|
220
|
-
}
|