paralayer 0.0.2 → 1.0.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/bin.js +3 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/paralayer.d.ts +46 -0
- package/dist/paralayer.js +237 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +4 -0
- package/package.json +39 -5
- package/src/paralayer.ts +0 -1
package/bin.js
ADDED
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import minimist from 'minimist';
|
|
2
|
+
import { Paralayer } from './paralayer.js';
|
|
3
|
+
const argv = minimist(process.argv.slice(2), {
|
|
4
|
+
string: ['default'],
|
|
5
|
+
boolean: ['watch', 'globalize'],
|
|
6
|
+
default: { default: null, watch: false, 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
|
+
const paralayer = new Paralayer({
|
|
20
|
+
input: input,
|
|
21
|
+
output: output,
|
|
22
|
+
globalize: argv.globalize,
|
|
23
|
+
default: argv.default,
|
|
24
|
+
});
|
|
25
|
+
await paralayer.start({
|
|
26
|
+
watch: argv.watch,
|
|
27
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Paralayer, type Options } from './paralayer.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Paralayer } from './paralayer.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as $utils from '@eposlabs/utils';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
export type DirPath = string;
|
|
4
|
+
export type File = {
|
|
5
|
+
content: string;
|
|
6
|
+
names: string[];
|
|
7
|
+
};
|
|
8
|
+
export type Options = {
|
|
9
|
+
input: DirPath | DirPath[];
|
|
10
|
+
output: DirPath;
|
|
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
|
+
private viteConfig;
|
|
27
|
+
constructor(options: Options);
|
|
28
|
+
get vite(): Plugin;
|
|
29
|
+
start: ({ watch }?: {
|
|
30
|
+
watch?: boolean | undefined;
|
|
31
|
+
}) => Promise<void>;
|
|
32
|
+
private onConfigResolved;
|
|
33
|
+
private onBuildStart;
|
|
34
|
+
private onAll;
|
|
35
|
+
private onReady;
|
|
36
|
+
private build;
|
|
37
|
+
private extractExportedClassNames;
|
|
38
|
+
private generateLayerContent;
|
|
39
|
+
private generateIndexContent;
|
|
40
|
+
private generateSetupContent;
|
|
41
|
+
private getLayer;
|
|
42
|
+
private getLayerName;
|
|
43
|
+
private capitalize;
|
|
44
|
+
private decapitalize;
|
|
45
|
+
private isTopLayer;
|
|
46
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
viteConfig = null;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super();
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
get vite() {
|
|
21
|
+
return {
|
|
22
|
+
name: 'paralayer',
|
|
23
|
+
enforce: 'pre',
|
|
24
|
+
configResolved: this.onConfigResolved,
|
|
25
|
+
buildStart: this.onBuildStart,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
start = async ({ watch = false } = {}) => {
|
|
29
|
+
if (this.started)
|
|
30
|
+
return;
|
|
31
|
+
this.started = true;
|
|
32
|
+
await $utils.safe($fs.rm(this.options.output, { recursive: true }));
|
|
33
|
+
this.watcher = $chokidar.watch(this.options.input);
|
|
34
|
+
this.watcher.on('all', this.onAll);
|
|
35
|
+
this.watcher.on('ready', this.onReady);
|
|
36
|
+
await this.ready$.promise;
|
|
37
|
+
await this.queue.run(() => this.build());
|
|
38
|
+
if (!watch)
|
|
39
|
+
await this.watcher.close();
|
|
40
|
+
};
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// VITE HOOKS
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
onConfigResolved = (config) => {
|
|
45
|
+
this.viteConfig = config;
|
|
46
|
+
};
|
|
47
|
+
onBuildStart = async () => {
|
|
48
|
+
if (!this.viteConfig)
|
|
49
|
+
throw this.never;
|
|
50
|
+
await this.start({ watch: !!this.viteConfig.build.watch });
|
|
51
|
+
};
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// HANDLERS
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
onAll = async (event, path) => {
|
|
56
|
+
// Process only file events
|
|
57
|
+
if (!['add', 'change', 'unlink'].includes(event))
|
|
58
|
+
return;
|
|
59
|
+
// Not supported file extension? -> Ignore
|
|
60
|
+
const ext = $path.extname(path);
|
|
61
|
+
if (!this.extensions.has(ext))
|
|
62
|
+
return;
|
|
63
|
+
// No layer in the file name? -> Ignore
|
|
64
|
+
if (!this.getLayer(path))
|
|
65
|
+
return;
|
|
66
|
+
// Initial scan? -> Just register file
|
|
67
|
+
if (!this.ready) {
|
|
68
|
+
this.files[path] = null;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// File removed? -> Remove and rebuild
|
|
72
|
+
if (event === 'unlink') {
|
|
73
|
+
delete this.files[path];
|
|
74
|
+
await this.queue.run(() => this.build());
|
|
75
|
+
}
|
|
76
|
+
// File added/changed? -> Reset its content and rebuild
|
|
77
|
+
if (event === 'add' || event === 'change') {
|
|
78
|
+
this.files[path] = null;
|
|
79
|
+
await this.queue.run(() => this.build());
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
onReady = async () => {
|
|
83
|
+
this.ready = true;
|
|
84
|
+
this.ready$.resolve();
|
|
85
|
+
};
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// BUILD
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
async build() {
|
|
90
|
+
// Group file paths by layers
|
|
91
|
+
const paths = Object.keys(this.files);
|
|
92
|
+
if (paths.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const pathsByLayers = Object.groupBy(paths, path => this.getLayer(path));
|
|
95
|
+
const allLayers = Object.keys(pathsByLayers);
|
|
96
|
+
// Ensure output directory exists
|
|
97
|
+
await $fs.mkdir(this.options.output, { recursive: true });
|
|
98
|
+
// Ensure all files are read
|
|
99
|
+
await Promise.all(paths.map(async (path) => {
|
|
100
|
+
if (this.files[path])
|
|
101
|
+
return;
|
|
102
|
+
const content = await $fs.readFile(path, 'utf-8');
|
|
103
|
+
const names = this.extractExportedClassNames(content);
|
|
104
|
+
this.files[path] = { content, names };
|
|
105
|
+
}));
|
|
106
|
+
// Create layer files
|
|
107
|
+
for (const layer in pathsByLayers) {
|
|
108
|
+
// Generate layer.[layer].ts
|
|
109
|
+
const layerFile = $path.join(this.options.output, `layer.${layer}.ts`);
|
|
110
|
+
const layerPaths = pathsByLayers[layer].toSorted();
|
|
111
|
+
const layerContent = this.generateLayerContent(layer, layerPaths);
|
|
112
|
+
await $fs.writeFile(layerFile, layerContent, 'utf-8');
|
|
113
|
+
// Top layer? -> Generate index.[layer].ts
|
|
114
|
+
if (this.isTopLayer(layer)) {
|
|
115
|
+
const indexFile = $path.join(this.options.output, `index.${layer}.ts`);
|
|
116
|
+
const indexContent = this.generateIndexContent(layer, allLayers);
|
|
117
|
+
await $fs.writeFile(indexFile, indexContent, 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Determine removed layers
|
|
121
|
+
const removedLayers = this.previousLayers.filter(l => !allLayers.includes(l));
|
|
122
|
+
this.previousLayers = allLayers;
|
|
123
|
+
// Delete files of removed layers
|
|
124
|
+
for (const layer of removedLayers) {
|
|
125
|
+
// Delete layer file
|
|
126
|
+
const layerFile = $path.join(this.options.output, `layer.${layer}.ts`);
|
|
127
|
+
await $fs.rm(layerFile);
|
|
128
|
+
// Top layer? -> Remove index file
|
|
129
|
+
if (this.isTopLayer(layer)) {
|
|
130
|
+
const indexFile = $path.join(this.options.output, `index.${layer}.ts`);
|
|
131
|
+
await $fs.rm(indexFile);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Generate setup.js file
|
|
135
|
+
const setupFile = $path.join(this.options.output, 'setup.js');
|
|
136
|
+
const setupContent = this.generateSetupContent(allLayers);
|
|
137
|
+
await $fs.writeFile(setupFile, setupContent, 'utf-8');
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// HELPERS
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
extractExportedClassNames(content) {
|
|
143
|
+
return content
|
|
144
|
+
.split('export class ')
|
|
145
|
+
.slice(1)
|
|
146
|
+
.map(part => part.split(' ')[0].split('<')[0]);
|
|
147
|
+
}
|
|
148
|
+
generateLayerContent(layer, layerPaths) {
|
|
149
|
+
const $LayerName = this.getLayerName(layer, '$Pascal');
|
|
150
|
+
const $layerName = this.getLayerName(layer, '$camel');
|
|
151
|
+
const allNames = layerPaths.flatMap(path => this.files[path].names);
|
|
152
|
+
const imports = layerPaths
|
|
153
|
+
.map(path => {
|
|
154
|
+
const file = this.files[path];
|
|
155
|
+
if (!file)
|
|
156
|
+
throw this.never;
|
|
157
|
+
if (file.names.length === 0)
|
|
158
|
+
return '';
|
|
159
|
+
const names = file.names;
|
|
160
|
+
const types = file.names.map(name => `type ${name} as ${name}Type`);
|
|
161
|
+
const relativePath = $path.relative(this.options.output, path);
|
|
162
|
+
return `import { ${[...names, ...types].join(', ')} } from '${relativePath}'`;
|
|
163
|
+
})
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
const assign = [`Object.assign(${$layerName}, {`, ...allNames.map(name => ` ${name},`), `})`];
|
|
166
|
+
const globals = [
|
|
167
|
+
`declare global {`,
|
|
168
|
+
` var ${$layerName}: ${$LayerName}`,
|
|
169
|
+
``,
|
|
170
|
+
` interface ${$LayerName} {`,
|
|
171
|
+
...allNames.map(name => ` ${name}: typeof ${name}`),
|
|
172
|
+
` }`,
|
|
173
|
+
``,
|
|
174
|
+
` namespace ${$layerName} {`,
|
|
175
|
+
...allNames.map(name => ` export type ${name} = ${name}Type`),
|
|
176
|
+
` }`,
|
|
177
|
+
`}`,
|
|
178
|
+
];
|
|
179
|
+
return [...imports, '', ...assign, '', ...globals, ''].join('\n');
|
|
180
|
+
}
|
|
181
|
+
generateIndexContent(topLayer, allLayers) {
|
|
182
|
+
const imports = allLayers
|
|
183
|
+
.filter(layer => layer.includes(topLayer))
|
|
184
|
+
.sort((layer1, layer2) => {
|
|
185
|
+
if (layer1.length !== layer2.length)
|
|
186
|
+
return layer2.length - layer1.length;
|
|
187
|
+
return layer1.localeCompare(layer2);
|
|
188
|
+
})
|
|
189
|
+
.map(layer => `import './layer.${layer}.ts'`);
|
|
190
|
+
return [...imports].join('\n');
|
|
191
|
+
}
|
|
192
|
+
generateSetupContent(allLayers) {
|
|
193
|
+
const nocheck = '// @ts-nocheck';
|
|
194
|
+
const layers = allLayers.toSorted((layer1, layer2) => {
|
|
195
|
+
if (layer1.length !== layer2.length)
|
|
196
|
+
return layer1.length - layer2.length;
|
|
197
|
+
return layer1.localeCompare(layer2);
|
|
198
|
+
});
|
|
199
|
+
const vars = layers.map(layer => {
|
|
200
|
+
const $layerName = this.getLayerName(layer, '$camel');
|
|
201
|
+
return `const ${$layerName} = {}`;
|
|
202
|
+
});
|
|
203
|
+
let globals = [];
|
|
204
|
+
if (this.options.globalize) {
|
|
205
|
+
globals = layers.map(layer => {
|
|
206
|
+
const $layerName = this.getLayerName(layer, '$camel');
|
|
207
|
+
return `globalThis.${$layerName} = ${$layerName}`;
|
|
208
|
+
});
|
|
209
|
+
globals.unshift('');
|
|
210
|
+
}
|
|
211
|
+
return [nocheck, '', ...vars, ...globals, ''].join('\n');
|
|
212
|
+
}
|
|
213
|
+
getLayer(path) {
|
|
214
|
+
const name = $path.basename(path);
|
|
215
|
+
const layer = name.split('.').slice(1, -1).sort().join('.');
|
|
216
|
+
if (layer)
|
|
217
|
+
return layer;
|
|
218
|
+
return this.options.default ?? '';
|
|
219
|
+
}
|
|
220
|
+
getLayerName(layer, style) {
|
|
221
|
+
const LayerName = layer.split('.').map(this.capitalize).join('');
|
|
222
|
+
if (style === '$camel')
|
|
223
|
+
return `$${this.decapitalize(LayerName)}`;
|
|
224
|
+
if (style === '$Pascal')
|
|
225
|
+
return `$${LayerName}`;
|
|
226
|
+
throw this.never;
|
|
227
|
+
}
|
|
228
|
+
capitalize(string) {
|
|
229
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
230
|
+
}
|
|
231
|
+
decapitalize(string) {
|
|
232
|
+
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
233
|
+
}
|
|
234
|
+
isTopLayer(layer) {
|
|
235
|
+
return !layer.includes('.');
|
|
236
|
+
}
|
|
237
|
+
}
|
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paralayer",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "imkost",
|
|
7
|
+
"description": "",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"vite",
|
|
10
|
+
"vite-plugin"
|
|
11
|
+
],
|
|
4
12
|
"scripts": {
|
|
5
|
-
"
|
|
6
|
-
"
|
|
13
|
+
"dev": "rm -rf dist && tsc --watch",
|
|
14
|
+
"build": "rm -rf dist && tsc",
|
|
15
|
+
"lint": "tsc --noEmit",
|
|
16
|
+
"release": "npm version patch && npm run release:raw",
|
|
17
|
+
"release:raw": "npm run build && npm publish",
|
|
18
|
+
"release:minor": "npm version minor && npm run release:raw"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"paralayer": "./bin.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"default": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./vite": {
|
|
29
|
+
"default": "./dist/vite.js",
|
|
30
|
+
"types": "./dist/vite.d.ts"
|
|
31
|
+
}
|
|
7
32
|
},
|
|
8
33
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
|
|
34
|
+
"dist",
|
|
35
|
+
"bin.js"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chalk": "^5.6.0",
|
|
39
|
+
"chokidar": "^4.0.3",
|
|
40
|
+
"minimist": "^1.2.8"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/minimist": "^1.2.5"
|
|
44
|
+
}
|
|
11
45
|
}
|
package/src/paralayer.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export class Paralayer {}
|