@fluffjs/cli 0.0.1

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.
@@ -0,0 +1,28 @@
1
+ import type { ClassTransformOptions } from './babel-plugin-class-transform.js';
2
+ import type { ComponentMetadata } from './babel-plugin-component.js';
3
+ export interface CompileResult {
4
+ code: string;
5
+ map?: string;
6
+ watchFiles?: string[];
7
+ }
8
+ export declare class ComponentCompiler {
9
+ private readonly componentSelectors;
10
+ private readonly parser;
11
+ private readonly generator;
12
+ constructor();
13
+ registerComponent(selector: string): void;
14
+ discoverComponents(demoDir: string): Promise<void>;
15
+ compileComponent(filePath: string): Promise<string>;
16
+ compileComponentForBundle(filePath: string, minify?: boolean, sourcemap?: boolean): Promise<CompileResult>;
17
+ stripTypeScriptWithSourceMap(code: string, filePath: string, sourcemap?: boolean): Promise<CompileResult>;
18
+ private createComponentSourceMap;
19
+ transformImportsForBundle(code: string, filePath: string): Promise<string>;
20
+ transformReactiveProperties(code: string, filePath?: string): Promise<string>;
21
+ stripTypeScript(code: string, filePath?: string): Promise<string>;
22
+ extractComponentMetadata(code: string, filePath: string): Promise<ComponentMetadata | null>;
23
+ transformImportsAndDecorators(code: string, filePath: string): Promise<string>;
24
+ transformClass(code: string, filePath: string, options: ClassTransformOptions): Promise<string>;
25
+ transformLibraryImports(code: string, filePath: string): Promise<string>;
26
+ copyLighterLib(srcDir: string, distDir: string): Promise<void>;
27
+ }
28
+ //# sourceMappingURL=ComponentCompiler.d.ts.map
@@ -0,0 +1,354 @@
1
+ import * as babel from '@babel/core';
2
+ import * as esbuild from 'esbuild';
3
+ import * as fs from 'fs';
4
+ import { minify as minifyHtml } from 'html-minifier-terser';
5
+ import * as path from 'path';
6
+ import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
7
+ import classTransformPlugin, { injectMethodBodies } from './babel-plugin-class-transform.js';
8
+ import componentPlugin, { componentMetadataMap } from './babel-plugin-component.js';
9
+ import importsPlugin from './babel-plugin-imports.js';
10
+ import reactivePlugin, { reactivePropertiesMap } from './babel-plugin-reactive.js';
11
+ import { CodeGenerator } from './CodeGenerator.js';
12
+ import { TemplateParser } from './TemplateParser.js';
13
+ export class ComponentCompiler {
14
+ componentSelectors = new Set();
15
+ parser;
16
+ generator;
17
+ constructor() {
18
+ this.parser = new TemplateParser();
19
+ this.generator = new CodeGenerator();
20
+ }
21
+ registerComponent(selector) {
22
+ this.componentSelectors.add(selector);
23
+ }
24
+ async discoverComponents(demoDir) {
25
+ const files = fs.readdirSync(demoDir)
26
+ .filter(f => f.endsWith('.component.ts'));
27
+ for (const file of files) {
28
+ const filePath = path.join(demoDir, file);
29
+ const content = fs.readFileSync(filePath, 'utf-8');
30
+ const metadata = await this.extractComponentMetadata(content, filePath);
31
+ if (metadata?.selector) {
32
+ this.componentSelectors.add(metadata.selector);
33
+ }
34
+ }
35
+ }
36
+ async compileComponent(filePath) {
37
+ let source = fs.readFileSync(filePath, 'utf-8');
38
+ const componentDir = path.dirname(filePath);
39
+ if (source.includes('@Reactive') || source.includes('@Input')) {
40
+ source = await this.transformReactiveProperties(source, filePath);
41
+ const reactiveProps = reactivePropertiesMap.get(filePath);
42
+ if (reactiveProps) {
43
+ this.generator.setReactiveProperties(reactiveProps);
44
+ }
45
+ }
46
+ else {
47
+ this.generator.setReactiveProperties(new Set());
48
+ }
49
+ const metadata = await this.extractComponentMetadata(source, filePath);
50
+ if (!metadata) {
51
+ return source;
52
+ }
53
+ const { selector, templateUrl, styleUrl, className } = metadata;
54
+ const templatePath = path.resolve(componentDir, templateUrl);
55
+ const templateHtml = fs.readFileSync(templatePath, 'utf-8');
56
+ let styles = '';
57
+ if (styleUrl) {
58
+ const stylePath = path.resolve(componentDir, styleUrl);
59
+ if (fs.existsSync(stylePath)) {
60
+ styles = fs.readFileSync(stylePath, 'utf-8');
61
+ }
62
+ }
63
+ const { html, bindings, controlFlows, templateRefs } = this.parser.parse(templateHtml);
64
+ this.generator.setTemplateRefs(templateRefs);
65
+ const renderMethod = this.generator.generateRenderMethod(html, styles);
66
+ const bindingsSetup = this.generator.generateBindingsSetup(bindings, controlFlows);
67
+ let result = await this.transformImportsAndDecorators(source, filePath);
68
+ result = await this.transformClass(result, filePath, {
69
+ className, originalSuperClass: 'HTMLElement', newSuperClass: 'FluffElement', injectMethods: [
70
+ { name: '__render', body: renderMethod }, { name: '__setupBindings', body: bindingsSetup }
71
+ ]
72
+ });
73
+ result = 'import { FluffElement } from \'./fluff-lib/runtime/FluffElement.js\';\n' + result;
74
+ result += `\ncustomElements.define('${selector}', ${className});\n`;
75
+ result = await this.stripTypeScript(result, filePath);
76
+ return result;
77
+ }
78
+ async compileComponentForBundle(filePath, minify, sourcemap) {
79
+ let source = fs.readFileSync(filePath, 'utf-8');
80
+ const componentDir = path.dirname(filePath);
81
+ const generator = new CodeGenerator();
82
+ if (source.includes('@Reactive') || source.includes('@Input')) {
83
+ source = await this.transformReactiveProperties(source, filePath);
84
+ const reactiveProps = reactivePropertiesMap.get(filePath);
85
+ if (reactiveProps) {
86
+ generator.setReactiveProperties(reactiveProps);
87
+ }
88
+ }
89
+ else {
90
+ generator.setReactiveProperties(new Set());
91
+ }
92
+ const metadata = await this.extractComponentMetadata(source, filePath);
93
+ if (!metadata) {
94
+ return { code: source };
95
+ }
96
+ const { selector, templateUrl, styleUrl, className } = metadata;
97
+ const templatePath = path.resolve(componentDir, templateUrl);
98
+ let templateHtml = fs.readFileSync(templatePath, 'utf-8');
99
+ const stylePath = styleUrl ? path.resolve(componentDir, styleUrl) : null;
100
+ let styles = '';
101
+ if (stylePath && fs.existsSync(stylePath)) {
102
+ styles = fs.readFileSync(stylePath, 'utf-8');
103
+ }
104
+ if (minify) {
105
+ templateHtml = await minifyHtml(templateHtml, {
106
+ collapseWhitespace: true,
107
+ removeComments: true,
108
+ removeRedundantAttributes: true,
109
+ removeEmptyAttributes: true
110
+ });
111
+ if (styles) {
112
+ const cssResult = await esbuild.transform(styles, {
113
+ loader: 'css',
114
+ minify: true
115
+ });
116
+ styles = cssResult.code;
117
+ }
118
+ }
119
+ const { html, bindings, controlFlows, templateRefs } = this.parser.parse(templateHtml);
120
+ generator.setTemplateRefs(templateRefs);
121
+ const renderMethod = generator.generateRenderMethod(html, styles);
122
+ const bindingsSetup = generator.generateBindingsSetup(bindings, controlFlows);
123
+ let result = await this.transformImportsForBundle(source, filePath);
124
+ result = await this.transformClass(result, filePath, {
125
+ className, originalSuperClass: 'HTMLElement', newSuperClass: 'FluffElement', injectMethods: [
126
+ { name: '__render', body: renderMethod }, { name: '__setupBindings', body: bindingsSetup }
127
+ ]
128
+ });
129
+ result = 'import { FluffElement } from \'@fluffjs/fluff\';\n' + result;
130
+ result += `\ncustomElements.define('${selector}', ${className});\n`;
131
+ const tsResult = await this.stripTypeScriptWithSourceMap(result, filePath, sourcemap);
132
+ const watchFiles = [templatePath];
133
+ if (stylePath && fs.existsSync(stylePath)) {
134
+ watchFiles.push(stylePath);
135
+ }
136
+ if (sourcemap && tsResult.map) {
137
+ const finalMap = await this.createComponentSourceMap(tsResult.code, tsResult.map, filePath, templatePath, stylePath);
138
+ return { code: tsResult.code, map: finalMap, watchFiles };
139
+ }
140
+ return { code: tsResult.code, watchFiles };
141
+ }
142
+ async stripTypeScriptWithSourceMap(code, filePath, sourcemap) {
143
+ try {
144
+ const result = await esbuild.transform(code, {
145
+ loader: 'ts',
146
+ format: 'esm',
147
+ target: 'es2022',
148
+ sourcemap: sourcemap ? 'external' : false,
149
+ sourcefile: filePath
150
+ });
151
+ return { code: result.code, map: result.map };
152
+ }
153
+ catch (e) {
154
+ const message = e instanceof Error ? e.message : String(e);
155
+ console.error(`Error transforming ${filePath}:`, message);
156
+ return { code };
157
+ }
158
+ }
159
+ async createComponentSourceMap(code, esbuildMap, componentPath, templatePath, stylePath) {
160
+ const consumer = await new SourceMapConsumer(JSON.parse(esbuildMap));
161
+ const generator = new SourceMapGenerator({
162
+ file: path.basename(componentPath)
163
+ .replace('.ts', '.js')
164
+ });
165
+ generator.setSourceContent(componentPath, fs.readFileSync(componentPath, 'utf-8'));
166
+ generator.setSourceContent(templatePath, fs.readFileSync(templatePath, 'utf-8'));
167
+ if (stylePath && fs.existsSync(stylePath)) {
168
+ generator.setSourceContent(stylePath, fs.readFileSync(stylePath, 'utf-8'));
169
+ }
170
+ consumer.eachMapping(mapping => {
171
+ if (mapping.source) {
172
+ generator.addMapping({
173
+ generated: { line: mapping.generatedLine, column: mapping.generatedColumn },
174
+ original: { line: mapping.originalLine, column: mapping.originalColumn },
175
+ source: mapping.source,
176
+ name: mapping.name ?? undefined
177
+ });
178
+ }
179
+ });
180
+ consumer.destroy();
181
+ return generator.toString();
182
+ }
183
+ async transformImportsForBundle(code, filePath) {
184
+ try {
185
+ const importOptions = {
186
+ removeImportsFrom: ['lighter'],
187
+ removeDecorators: ['Component', 'Input', 'Output'],
188
+ pathReplacements: {},
189
+ addJsExtension: false
190
+ };
191
+ const result = await babel.transformAsync(code, {
192
+ filename: filePath, presets: [
193
+ ['@babel/preset-typescript', { isTSX: false, allExtensions: true }]
194
+ ], plugins: [
195
+ ['@babel/plugin-syntax-decorators', { version: '2023-11' }], [importsPlugin, importOptions]
196
+ ], parserOpts: {
197
+ plugins: ['typescript', 'decorators']
198
+ }
199
+ });
200
+ return result?.code ?? code;
201
+ }
202
+ catch (e) {
203
+ const message = e instanceof Error ? e.message : String(e);
204
+ console.error(`Failed to transform imports in ${filePath}:`, message);
205
+ return code;
206
+ }
207
+ }
208
+ async transformReactiveProperties(code, filePath = 'file.ts') {
209
+ try {
210
+ const result = await babel.transformAsync(code, {
211
+ filename: filePath, presets: [
212
+ ['@babel/preset-typescript', { isTSX: false, allExtensions: true }]
213
+ ], plugins: [
214
+ ['@babel/plugin-syntax-decorators', { version: '2023-11' }], reactivePlugin
215
+ ], parserOpts: {
216
+ plugins: ['typescript', 'decorators']
217
+ }
218
+ });
219
+ return result?.code ?? code;
220
+ }
221
+ catch (e) {
222
+ const message = e instanceof Error ? e.message : String(e);
223
+ console.error(`Babel transform error in ${filePath}:`, message);
224
+ return code;
225
+ }
226
+ }
227
+ async stripTypeScript(code, filePath = 'file.ts') {
228
+ try {
229
+ const result = await esbuild.transform(code, {
230
+ loader: 'ts', format: 'esm', target: 'es2022',
231
+ });
232
+ return result.code;
233
+ }
234
+ catch (e) {
235
+ const message = e instanceof Error ? e.message : String(e);
236
+ console.error(`Error transforming ${filePath}:`, message);
237
+ return code;
238
+ }
239
+ }
240
+ async extractComponentMetadata(code, filePath) {
241
+ try {
242
+ componentMetadataMap.delete(filePath);
243
+ await babel.transformAsync(code, {
244
+ filename: filePath, presets: [
245
+ ['@babel/preset-typescript', { isTSX: false, allExtensions: true }]
246
+ ], plugins: [
247
+ ['@babel/plugin-syntax-decorators', { version: '2023-11' }], componentPlugin
248
+ ], parserOpts: {
249
+ plugins: ['typescript', 'decorators']
250
+ }
251
+ });
252
+ return componentMetadataMap.get(filePath) ?? null;
253
+ }
254
+ catch (e) {
255
+ const message = e instanceof Error ? e.message : String(e);
256
+ console.error(`Failed to extract component metadata from ${filePath}:`, message);
257
+ return null;
258
+ }
259
+ }
260
+ async transformImportsAndDecorators(code, filePath) {
261
+ try {
262
+ const importOptions = {
263
+ removeImportsFrom: ['lighter'], removeDecorators: ['Component', 'Input', 'Output'], pathReplacements: {
264
+ '@/fluff-lib/': './fluff-lib/'
265
+ }, addJsExtension: true
266
+ };
267
+ const result = await babel.transformAsync(code, {
268
+ filename: filePath, presets: [
269
+ ['@babel/preset-typescript', { isTSX: false, allExtensions: true }]
270
+ ], plugins: [
271
+ ['@babel/plugin-syntax-decorators', { version: '2023-11' }], [importsPlugin, importOptions]
272
+ ], parserOpts: {
273
+ plugins: ['typescript', 'decorators']
274
+ }
275
+ });
276
+ return result?.code ?? code;
277
+ }
278
+ catch (e) {
279
+ const message = e instanceof Error ? e.message : String(e);
280
+ console.error(`Failed to transform imports in ${filePath}:`, message);
281
+ return code;
282
+ }
283
+ }
284
+ async transformClass(code, filePath, options) {
285
+ try {
286
+ const result = await babel.transformAsync(code, {
287
+ filename: filePath, plugins: [
288
+ [classTransformPlugin, options]
289
+ ]
290
+ });
291
+ let transformed = result?.code ?? code;
292
+ if (options.injectMethods) {
293
+ transformed = injectMethodBodies(transformed, options.injectMethods);
294
+ }
295
+ return transformed;
296
+ }
297
+ catch (e) {
298
+ const message = e instanceof Error ? e.message : String(e);
299
+ console.error(`Failed to transform class in ${filePath}:`, message);
300
+ return code;
301
+ }
302
+ }
303
+ async transformLibraryImports(code, filePath) {
304
+ try {
305
+ const importOptions = {
306
+ pathReplacements: {
307
+ '@/fluff-lib/': '../'
308
+ }, addJsExtension: true
309
+ };
310
+ const result = await babel.transformAsync(code, {
311
+ filename: filePath, presets: [
312
+ ['@babel/preset-typescript', { isTSX: false, allExtensions: true }]
313
+ ], plugins: [
314
+ [importsPlugin, importOptions]
315
+ ], parserOpts: {
316
+ plugins: ['typescript']
317
+ }
318
+ });
319
+ return result?.code ?? code;
320
+ }
321
+ catch (e) {
322
+ const message = e instanceof Error ? e.message : String(e);
323
+ console.error(`Failed to transform library imports in ${filePath}:`, message);
324
+ return code;
325
+ }
326
+ }
327
+ async copyLighterLib(srcDir, distDir) {
328
+ const lighterLibSrc = path.join(srcDir, 'fluff-lib');
329
+ const lighterLibDist = path.join(distDir, 'fluff-lib');
330
+ if (!fs.existsSync(lighterLibDist)) {
331
+ fs.mkdirSync(lighterLibDist, { recursive: true });
332
+ }
333
+ const dirs = ['utils', 'runtime', 'interfaces', 'enums', 'decorators'];
334
+ for (const dir of dirs) {
335
+ const srcPath = path.join(lighterLibSrc, dir);
336
+ const distPath = path.join(lighterLibDist, dir);
337
+ if (fs.existsSync(srcPath)) {
338
+ if (!fs.existsSync(distPath)) {
339
+ fs.mkdirSync(distPath, { recursive: true });
340
+ }
341
+ const files = fs.readdirSync(srcPath)
342
+ .filter(f => f.endsWith('.ts'));
343
+ for (const file of files) {
344
+ const fullPath = path.join(srcPath, file);
345
+ let content = fs.readFileSync(fullPath, 'utf-8');
346
+ content = await this.transformLibraryImports(content, fullPath);
347
+ content = await this.stripTypeScript(content, fullPath);
348
+ const outFile = file.replace('.ts', '.js');
349
+ fs.writeFileSync(path.join(distPath, outFile), content);
350
+ }
351
+ }
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,55 @@
1
+ export interface IfBlock {
2
+ type: 'if';
3
+ condition: string;
4
+ ifContent: string;
5
+ elseContent: string;
6
+ start: number;
7
+ end: number;
8
+ }
9
+ export interface ForBlock {
10
+ type: 'for';
11
+ iterator: string;
12
+ iterable: string;
13
+ trackBy?: string;
14
+ content: string;
15
+ start: number;
16
+ end: number;
17
+ }
18
+ export interface SwitchCase {
19
+ value: string | null;
20
+ content: string;
21
+ fallthrough: boolean;
22
+ }
23
+ export interface SwitchBlock {
24
+ type: 'switch';
25
+ expression: string;
26
+ cases: SwitchCase[];
27
+ start: number;
28
+ end: number;
29
+ }
30
+ export type ControlFlowBlock = IfBlock | ForBlock | SwitchBlock;
31
+ export declare class ControlFlowParser {
32
+ parseAll(input: string): {
33
+ result: string;
34
+ blocks: ControlFlowBlock[];
35
+ };
36
+ private findMatchingDelimiter;
37
+ private findMatchingBrace;
38
+ private extractParenContent;
39
+ private parseForExpression;
40
+ private parseBlockStructure;
41
+ parseSwitchBlocks(input: string): {
42
+ result: string;
43
+ blocks: SwitchBlock[];
44
+ };
45
+ private parseSwitchCases;
46
+ parseForBlocks(input: string): {
47
+ result: string;
48
+ blocks: ForBlock[];
49
+ };
50
+ parseIfBlocks(input: string): {
51
+ result: string;
52
+ blocks: IfBlock[];
53
+ };
54
+ }
55
+ //# sourceMappingURL=ControlFlowParser.d.ts.map