paralayer 0.0.2 → 0.0.4

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 ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import './dist/bin.js'
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
+ });
@@ -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
@@ -0,0 +1,2 @@
1
+ import { type Options } from './paralayer.js';
2
+ export default function paralayer(options: Options): import("vite").Plugin<any>;
package/dist/vite.js ADDED
@@ -0,0 +1,4 @@
1
+ import { Paralayer } from './paralayer.js';
2
+ export default function paralayer(options) {
3
+ return new Paralayer(options).vite;
4
+ }
package/package.json CHANGED
@@ -1,11 +1,45 @@
1
1
  {
2
2
  "name": "paralayer",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "author": "imkost",
7
+ "description": "",
8
+ "keywords": [
9
+ "vite",
10
+ "vite-plugin"
11
+ ],
4
12
  "scripts": {
5
- "build": "echo 'empty'",
6
- "release": "npm run build && npm version patch && npm publish"
13
+ "dev": "rm -rf dist && tsc --watch",
14
+ "build": "rm -rf dist && tsc",
15
+ "lint": "eslint src",
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
- "src"
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 {}