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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paralayer",
3
- "version": "1.0.5",
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.js"
19
+ "paralayer": "./src/bin.ts"
20
20
  },
21
21
  "exports": {
22
22
  ".": {
23
- "default": "./dist/index.js",
24
- "types": "./dist/index.d.ts"
23
+ "import": "./src/index.ts"
25
24
  }
26
25
  },
27
26
  "files": [
28
- "dist",
29
- "bin.js"
27
+ "src"
30
28
  ],
31
29
  "dependencies": {
32
- "@eposlabs/utils": "^1.0.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
@@ -0,0 +1,9 @@
1
+ import { Paralayer, type Options } from './paralayer.ts'
2
+
3
+ export async function paralayer(options: Options) {
4
+ const pl = new Paralayer(options)
5
+ await pl.start()
6
+ return await pl.readSetupJs()
7
+ }
8
+
9
+ export default paralayer
@@ -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
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import './dist/bin.js'
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
@@ -1,3 +0,0 @@
1
- import { type Options } from './paralayer.js';
2
- export declare function paralayer(options: Options): Promise<string>;
3
- export default paralayer;
package/dist/index.js DELETED
@@ -1,7 +0,0 @@
1
- import { Paralayer } from './paralayer.js';
2
- export async function paralayer(options) {
3
- const pl = new Paralayer(options);
4
- await pl.start();
5
- return await pl.readSetupJs();
6
- }
7
- export default paralayer;
@@ -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
- }