@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,279 @@
1
+ import { parse } from '@babel/parser';
2
+ import * as t from '@babel/types';
3
+ const OPENING_BRACE_PATTERN = /^\s*\{/;
4
+ const OPENING_BRACE_WITH_PAREN_PATTERN = /^\)?\s*\{/;
5
+ const FALLTHROUGH_PATTERN = /@fallthrough\s*$/;
6
+ export class ControlFlowParser {
7
+ parseAll(input) {
8
+ let result = input;
9
+ const allBlocks = [];
10
+ const switchResult = this.parseSwitchBlocks(result);
11
+ ({ result } = switchResult);
12
+ allBlocks.push(...switchResult.blocks);
13
+ const forResult = this.parseForBlocks(result);
14
+ ({ result } = forResult);
15
+ allBlocks.push(...forResult.blocks);
16
+ const ifResult = this.parseIfBlocks(result);
17
+ ({ result } = ifResult);
18
+ allBlocks.push(...ifResult.blocks);
19
+ return { result, blocks: allBlocks };
20
+ }
21
+ findMatchingDelimiter(str, openIdx, openChar, closeChar) {
22
+ let depth = 1;
23
+ let inString = false;
24
+ let i = openIdx + 1;
25
+ while (i < str.length && depth > 0) {
26
+ const char = str[i];
27
+ const prevChar = str[i - 1];
28
+ if (prevChar === '\\' && inString) {
29
+ i++;
30
+ continue;
31
+ }
32
+ if (!inString) {
33
+ if (char === '"' || char === '\'' || char === '`') {
34
+ inString = char;
35
+ }
36
+ else if (char === openChar) {
37
+ depth++;
38
+ }
39
+ else if (char === closeChar) {
40
+ depth--;
41
+ }
42
+ }
43
+ else {
44
+ if (char === inString) {
45
+ inString = false;
46
+ }
47
+ else if (inString === '`' && char === '$' && str[i + 1] === '{') {
48
+ i += 2;
49
+ let templateDepth = 1;
50
+ while (i < str.length && templateDepth > 0) {
51
+ const tc = str[i];
52
+ if (tc === '{')
53
+ templateDepth++;
54
+ else if (tc === '}')
55
+ templateDepth--;
56
+ else if (tc === '`') {
57
+ i++;
58
+ while (i < str.length && str[i] !== '`') {
59
+ if (str[i] === '\\')
60
+ i++;
61
+ i++;
62
+ }
63
+ }
64
+ i++;
65
+ }
66
+ continue;
67
+ }
68
+ }
69
+ i++;
70
+ }
71
+ return depth === 0 ? i - 1 : -1;
72
+ }
73
+ findMatchingBrace(str, openIdx) {
74
+ return this.findMatchingDelimiter(str, openIdx, '{', '}');
75
+ }
76
+ extractParenContent(str, startIdx) {
77
+ if (str[startIdx] !== '(')
78
+ return null;
79
+ const endIdx = this.findMatchingDelimiter(str, startIdx, '(', ')');
80
+ if (endIdx === -1)
81
+ return null;
82
+ return {
83
+ content: str.slice(startIdx + 1, endIdx), endIdx
84
+ };
85
+ }
86
+ parseForExpression(expr) {
87
+ let mainExpr = expr;
88
+ let trackBy = undefined;
89
+ const trackIndex = expr.indexOf('; track ');
90
+ if (trackIndex !== -1) {
91
+ mainExpr = expr.slice(0, trackIndex);
92
+ trackBy = expr.slice(trackIndex + 8)
93
+ .trim();
94
+ }
95
+ try {
96
+ const wrappedCode = `for (const ${mainExpr}) {}`;
97
+ const ast = parse(wrappedCode, { sourceType: 'module' });
98
+ const [stmt] = ast.program.body;
99
+ if (!t.isForOfStatement(stmt))
100
+ return null;
101
+ const { left } = stmt;
102
+ let iterator = '';
103
+ if (t.isVariableDeclaration(left) && left.declarations.length === 1) {
104
+ const [decl] = left.declarations;
105
+ if (t.isIdentifier(decl.id)) {
106
+ iterator = decl.id.name;
107
+ }
108
+ else {
109
+ iterator = mainExpr.slice(0, mainExpr.indexOf(' of '))
110
+ .trim();
111
+ }
112
+ }
113
+ else {
114
+ return null;
115
+ }
116
+ const ofIndex = mainExpr.indexOf(' of ');
117
+ if (ofIndex === -1)
118
+ return null;
119
+ const iterable = mainExpr.slice(ofIndex + 4)
120
+ .trim();
121
+ return { iterator, iterable, trackBy };
122
+ }
123
+ catch {
124
+ const ofIndex = mainExpr.indexOf(' of ');
125
+ if (ofIndex === -1)
126
+ return null;
127
+ const fallbackIterator = mainExpr.slice(0, ofIndex)
128
+ .trim();
129
+ const iterable = mainExpr.slice(ofIndex + 4)
130
+ .trim();
131
+ return { iterator: fallbackIterator, iterable, trackBy };
132
+ }
133
+ }
134
+ parseBlockStructure(input, match) {
135
+ const parenStart = match.index + match[0].length - 1;
136
+ const exprResult = this.extractParenContent(input, parenStart);
137
+ if (!exprResult)
138
+ return null;
139
+ const afterExpr = input.slice(exprResult.endIdx + 1);
140
+ const braceMatch = OPENING_BRACE_PATTERN.exec(afterExpr);
141
+ if (!braceMatch)
142
+ return null;
143
+ const braceStart = exprResult.endIdx + 1 + braceMatch[0].length - 1;
144
+ const braceEnd = this.findMatchingBrace(input, braceStart);
145
+ if (braceEnd === -1)
146
+ return null;
147
+ return {
148
+ expr: exprResult.content.trim(),
149
+ content: input.slice(braceStart + 1, braceEnd)
150
+ .trim(),
151
+ braceEnd
152
+ };
153
+ }
154
+ parseSwitchBlocks(input) {
155
+ const blocks = [];
156
+ let result = input;
157
+ const switchRegex = /@switch\s*\(/g;
158
+ let match = null;
159
+ while ((match = switchRegex.exec(result)) !== null) {
160
+ const startIdx = match.index;
161
+ const parsed = this.parseBlockStructure(result, match);
162
+ if (!parsed)
163
+ continue;
164
+ const cases = this.parseSwitchCases(parsed.content);
165
+ blocks.push({
166
+ type: 'switch', expression: parsed.expr, cases, start: startIdx, end: parsed.braceEnd
167
+ });
168
+ const placeholder = `__SWITCH_BLOCK_${blocks.length - 1}`;
169
+ result = result.slice(0, startIdx) + placeholder + result.slice(parsed.braceEnd + 1);
170
+ switchRegex.lastIndex = startIdx + placeholder.length;
171
+ }
172
+ return { result, blocks };
173
+ }
174
+ parseSwitchCases(switchBody) {
175
+ const cases = [];
176
+ const caseRegex = /@(case|default)\s*\(?/g;
177
+ let match = null;
178
+ while ((match = caseRegex.exec(switchBody)) !== null) {
179
+ const [, caseType] = match;
180
+ let value = null;
181
+ let contentStart = 0;
182
+ if (caseType === 'case') {
183
+ const parenIdx = switchBody.indexOf('(', match.index);
184
+ if (parenIdx === -1)
185
+ continue;
186
+ const valueResult = this.extractParenContent(switchBody, parenIdx);
187
+ if (!valueResult)
188
+ continue;
189
+ value = valueResult.content.trim();
190
+ const afterValue = switchBody.slice(valueResult.endIdx + 1);
191
+ const braceMatch = OPENING_BRACE_PATTERN.exec(afterValue);
192
+ if (!braceMatch)
193
+ continue;
194
+ contentStart = valueResult.endIdx + 1 + braceMatch[0].length;
195
+ }
196
+ else {
197
+ const afterDefault = switchBody.slice(match.index + match[0].length);
198
+ const braceMatch = OPENING_BRACE_WITH_PAREN_PATTERN.exec(afterDefault);
199
+ if (!braceMatch)
200
+ continue;
201
+ contentStart = match.index + match[0].length + braceMatch[0].length;
202
+ }
203
+ const contentEnd = this.findMatchingBrace(switchBody, contentStart - 1);
204
+ if (contentEnd === -1)
205
+ continue;
206
+ let content = switchBody.slice(contentStart, contentEnd);
207
+ const fallthrough = FALLTHROUGH_PATTERN.test(content.trim());
208
+ if (fallthrough) {
209
+ content = content.replace(FALLTHROUGH_PATTERN, '')
210
+ .trim();
211
+ }
212
+ cases.push({
213
+ value, content: content.trim(), fallthrough
214
+ });
215
+ caseRegex.lastIndex = contentEnd + 1;
216
+ }
217
+ return cases;
218
+ }
219
+ parseForBlocks(input) {
220
+ const blocks = [];
221
+ let result = input;
222
+ const forRegex = /@for\s*\(/g;
223
+ let match = null;
224
+ while ((match = forRegex.exec(result)) !== null) {
225
+ const startIdx = match.index;
226
+ const parsed = this.parseBlockStructure(result, match);
227
+ if (!parsed)
228
+ continue;
229
+ const forParts = this.parseForExpression(parsed.expr);
230
+ if (!forParts)
231
+ continue;
232
+ const { iterator, iterable, trackBy } = forParts;
233
+ blocks.push({
234
+ type: 'for', iterator, iterable, trackBy, content: parsed.content, start: startIdx, end: parsed.braceEnd
235
+ });
236
+ const placeholder = `__FOR_BLOCK_${blocks.length - 1}__`;
237
+ result = result.slice(0, startIdx) + placeholder + result.slice(parsed.braceEnd + 1);
238
+ forRegex.lastIndex = startIdx + placeholder.length;
239
+ }
240
+ return { result, blocks };
241
+ }
242
+ parseIfBlocks(input) {
243
+ const blocks = [];
244
+ let result = input;
245
+ const ifRegex = /@if\s*\(/g;
246
+ let match = null;
247
+ while ((match = ifRegex.exec(result)) !== null) {
248
+ const startIdx = match.index;
249
+ const parsed = this.parseBlockStructure(result, match);
250
+ if (!parsed)
251
+ continue;
252
+ let elseContent = '';
253
+ let totalEnd = parsed.braceEnd;
254
+ const afterIf = result.slice(parsed.braceEnd + 1);
255
+ const elseMatch = /^\s*@else\s*\{/.exec(afterIf);
256
+ if (elseMatch) {
257
+ const elseBraceStart = parsed.braceEnd + 1 + elseMatch[0].length - 1;
258
+ const elseBraceEnd = this.findMatchingBrace(result, elseBraceStart);
259
+ if (elseBraceEnd !== -1) {
260
+ elseContent = result.slice(elseBraceStart + 1, elseBraceEnd)
261
+ .trim();
262
+ totalEnd = elseBraceEnd;
263
+ }
264
+ }
265
+ blocks.push({
266
+ type: 'if',
267
+ condition: parsed.expr,
268
+ ifContent: parsed.content,
269
+ elseContent,
270
+ start: startIdx,
271
+ end: totalEnd
272
+ });
273
+ const placeholder = `__IF_BLOCK_${blocks.length - 1}__`;
274
+ result = result.slice(0, startIdx) + placeholder + result.slice(totalEnd + 1);
275
+ ifRegex.lastIndex = startIdx + placeholder.length;
276
+ }
277
+ return { result, blocks };
278
+ }
279
+ }
@@ -0,0 +1,30 @@
1
+ export interface TransformOptions {
2
+ addThisPrefix?: boolean;
3
+ nullSafe?: boolean;
4
+ iteratorName?: string;
5
+ iteratorReplacement?: string;
6
+ localVars?: string[];
7
+ }
8
+ export declare function transformExpression(expr: string, options?: TransformOptions): string;
9
+ export declare function addThisPrefix(expr: string): string;
10
+ export declare function addThisPrefixSafe(expr: string): string;
11
+ export declare function transformForExpression(expr: string, iteratorName: string, iteratorReplacement: string): string;
12
+ export declare function transformForExpressionKeepIterator(expr: string, iteratorName: string): string;
13
+ export declare function parsePipedExpression(expr: string): {
14
+ expression: string;
15
+ pipes: {
16
+ name: string;
17
+ args: string[];
18
+ }[];
19
+ };
20
+ export declare function transformInterpolation(expr: string, iteratorVar?: string): string;
21
+ export declare function extractRootIdentifier(expr: string): string | null;
22
+ export declare function parseInterpolations(text: string): {
23
+ start: number;
24
+ end: number;
25
+ expr: string;
26
+ }[];
27
+ export declare function transformPipedExpression(expr: string, addThisPrefixToExpr?: boolean): string;
28
+ export declare function renameVariable(expr: string, oldName: string, newName: string): string;
29
+ export declare function expressionUsesVariable(expr: string, varName: string): boolean;
30
+ //# sourceMappingURL=ExpressionTransformer.d.ts.map
@@ -0,0 +1,276 @@
1
+ import _generate from '@babel/generator';
2
+ import { parse } from '@babel/parser';
3
+ import _traverse from '@babel/traverse';
4
+ import * as t from '@babel/types';
5
+ const traverse = _traverse.default ?? _traverse;
6
+ const generate = _generate.default ?? _generate;
7
+ export function transformExpression(expr, options = {}) {
8
+ const { addThisPrefix: shouldAddThisPrefix = true, nullSafe = false, iteratorName, iteratorReplacement, localVars = [] } = options;
9
+ try {
10
+ const ast = parse(`(${expr})`, {
11
+ sourceType: 'module', plugins: ['typescript']
12
+ });
13
+ traverse(ast, {
14
+ Identifier(path) {
15
+ const { name } = path.node;
16
+ if (['true', 'false', 'null', 'undefined', 'this'].includes(name)) {
17
+ return;
18
+ }
19
+ if (localVars.includes(name)) {
20
+ return;
21
+ }
22
+ if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
23
+ return;
24
+ }
25
+ if (t.isOptionalMemberExpression(path.parent) && path.parent.property === path.node) {
26
+ return;
27
+ }
28
+ if (iteratorName && name === iteratorName && iteratorReplacement) {
29
+ const replacementAst = parse(`(${iteratorReplacement})`, { sourceType: 'module' });
30
+ const [firstStmt] = replacementAst.program.body;
31
+ if (!t.isExpressionStatement(firstStmt))
32
+ return;
33
+ const replacementExpr = firstStmt.expression;
34
+ path.replaceWith(replacementExpr);
35
+ return;
36
+ }
37
+ if (shouldAddThisPrefix) {
38
+ path.replaceWith(t.memberExpression(t.thisExpression(), t.identifier(name)));
39
+ }
40
+ }
41
+ });
42
+ if (nullSafe) {
43
+ traverse(ast, {
44
+ MemberExpression: {
45
+ exit(path) {
46
+ const obj = path.node.object;
47
+ const isThisMember = t.isMemberExpression(obj) && t.isThisExpression(obj.object);
48
+ const isOptionalChain = t.isOptionalMemberExpression(obj);
49
+ const isDeepChain = t.isMemberExpression(obj);
50
+ if (!path.node.optional && (isThisMember || isOptionalChain || isDeepChain)) {
51
+ const prop = path.node.property;
52
+ if (t.isExpression(prop)) {
53
+ const newNode = t.optionalMemberExpression(path.node.object, prop, path.node.computed, true);
54
+ path.replaceWith(newNode);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ });
60
+ }
61
+ const output = generate(ast, { compact: true });
62
+ let { code } = output;
63
+ if (code.startsWith('(') && code.endsWith(');')) {
64
+ code = code.slice(1, -2);
65
+ }
66
+ else if (code.startsWith('(') && code.endsWith(')')) {
67
+ code = code.slice(1, -1);
68
+ }
69
+ code = code.replace(/;+$/, '');
70
+ return code;
71
+ }
72
+ catch (e) {
73
+ const message = e instanceof Error ? e.message : String(e);
74
+ throw new Error(`Failed to parse expression "${expr}": ${message}`);
75
+ }
76
+ }
77
+ export function addThisPrefix(expr) {
78
+ return transformExpression(expr, { addThisPrefix: true, nullSafe: false });
79
+ }
80
+ export function addThisPrefixSafe(expr) {
81
+ return transformExpression(expr, { addThisPrefix: true, nullSafe: true });
82
+ }
83
+ export function transformForExpression(expr, iteratorName, iteratorReplacement) {
84
+ return transformExpression(expr, {
85
+ addThisPrefix: true,
86
+ nullSafe: false,
87
+ iteratorName,
88
+ iteratorReplacement,
89
+ localVars: ['__items', '__idx', '__item']
90
+ });
91
+ }
92
+ export function transformForExpressionKeepIterator(expr, iteratorName) {
93
+ return transformExpression(expr, {
94
+ addThisPrefix: true, nullSafe: false, localVars: [iteratorName, '__items', '__idx', '__item', '__currentItems']
95
+ });
96
+ }
97
+ export function parsePipedExpression(expr) {
98
+ let remaining = expr.trim();
99
+ const pipes = [];
100
+ while (true) {
101
+ let pipeIdx = -1;
102
+ for (let i = remaining.length - 1; i >= 0; i--) {
103
+ if (remaining[i] === '|') {
104
+ if (remaining[i - 1] === '|' || remaining[i + 1] === '|')
105
+ continue;
106
+ const left = remaining.slice(0, i)
107
+ .trim();
108
+ if (!left)
109
+ continue;
110
+ try {
111
+ parse(left, { sourceType: 'module' });
112
+ pipeIdx = i;
113
+ break;
114
+ }
115
+ catch {
116
+ }
117
+ }
118
+ }
119
+ if (pipeIdx === -1)
120
+ break;
121
+ const pipeExpr = remaining.slice(pipeIdx + 1)
122
+ .trim();
123
+ const colonParts = pipeExpr.split(':');
124
+ const pipeName = colonParts[0].trim();
125
+ const args = colonParts.slice(1)
126
+ .map(s => s.trim());
127
+ pipes.unshift({ name: pipeName, args });
128
+ remaining = remaining.slice(0, pipeIdx)
129
+ .trim();
130
+ }
131
+ return { expression: remaining, pipes };
132
+ }
133
+ export function transformInterpolation(expr, iteratorVar) {
134
+ const localVars = iteratorVar ? [iteratorVar] : [];
135
+ const { expression: baseExpr, pipes } = parsePipedExpression(expr);
136
+ if (pipes.length > 0) {
137
+ let result = transformExpression(baseExpr, { addThisPrefix: true, nullSafe: true, localVars });
138
+ for (const pipe of pipes) {
139
+ const argsStr = pipe.args.length > 0 ? ', ' + pipe.args.join(', ') : '';
140
+ result = `this.__pipe('${pipe.name}', ${result}${argsStr})`;
141
+ }
142
+ return `${result} ?? ''`;
143
+ }
144
+ const transformed = transformExpression(expr, { addThisPrefix: true, nullSafe: true, localVars });
145
+ return `${transformed} ?? ''`;
146
+ }
147
+ export function extractRootIdentifier(expr) {
148
+ try {
149
+ const ast = parse(expr, { sourceType: 'module' });
150
+ const [stmt] = ast.program.body;
151
+ if (!t.isExpressionStatement(stmt))
152
+ return null;
153
+ let node = stmt.expression;
154
+ while (true) {
155
+ if (t.isIdentifier(node)) {
156
+ return node.name;
157
+ }
158
+ else if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
159
+ node = node.object;
160
+ }
161
+ else if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
162
+ node = node.callee;
163
+ }
164
+ else if (t.isBinaryExpression(node) || t.isLogicalExpression(node)) {
165
+ node = node.left;
166
+ }
167
+ else if (t.isUnaryExpression(node)) {
168
+ node = node.argument;
169
+ }
170
+ else if (t.isConditionalExpression(node)) {
171
+ node = node.test;
172
+ }
173
+ else {
174
+ return null;
175
+ }
176
+ }
177
+ }
178
+ catch (e) {
179
+ const message = e instanceof Error ? e.message : String(e);
180
+ throw new Error(`Failed to parse expression "${expr}": ${message}`);
181
+ }
182
+ }
183
+ export function parseInterpolations(text) {
184
+ const results = [];
185
+ let searchStart = 0;
186
+ while (searchStart < text.length) {
187
+ const openIdx = text.indexOf('{{', searchStart);
188
+ if (openIdx === -1)
189
+ break;
190
+ let foundValid = false;
191
+ for (let closeIdx = openIdx + 3; closeIdx <= text.length; closeIdx++) {
192
+ if (text[closeIdx - 2] !== '}' || text[closeIdx - 1] !== '}')
193
+ continue;
194
+ const candidate = text.slice(openIdx + 2, closeIdx - 2)
195
+ .trim();
196
+ if (!candidate)
197
+ continue;
198
+ const { expression: baseExpr } = parsePipedExpression(candidate);
199
+ try {
200
+ parse(baseExpr, { sourceType: 'module' });
201
+ results.push({
202
+ start: openIdx, end: closeIdx, expr: candidate
203
+ });
204
+ searchStart = closeIdx;
205
+ foundValid = true;
206
+ break;
207
+ }
208
+ catch {
209
+ }
210
+ }
211
+ if (!foundValid) {
212
+ searchStart = openIdx + 2;
213
+ }
214
+ }
215
+ return results;
216
+ }
217
+ export function transformPipedExpression(expr, addThisPrefixToExpr = true) {
218
+ const { expression, pipes } = parsePipedExpression(expr);
219
+ if (pipes.length === 0) {
220
+ return expression;
221
+ }
222
+ let result = addThisPrefixToExpr ? transformExpression(expression, {
223
+ addThisPrefix: true,
224
+ nullSafe: true
225
+ }) : expression;
226
+ for (const pipe of pipes) {
227
+ const argsStr = pipe.args.length > 0 ? ', ' + pipe.args.join(', ') : '';
228
+ result = `this.__pipe('${pipe.name}', ${result}${argsStr})`;
229
+ }
230
+ return result;
231
+ }
232
+ export function renameVariable(expr, oldName, newName) {
233
+ try {
234
+ const ast = parse(expr, { sourceType: 'module' });
235
+ traverse(ast, {
236
+ Identifier(path) {
237
+ if (path.node.name === oldName) {
238
+ if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
239
+ return;
240
+ }
241
+ path.node.name = newName;
242
+ }
243
+ }
244
+ });
245
+ let { code } = generate(ast, { compact: false });
246
+ if (code.endsWith(';'))
247
+ code = code.slice(0, -1);
248
+ return code;
249
+ }
250
+ catch (e) {
251
+ const message = e instanceof Error ? e.message : String(e);
252
+ throw new Error(`Failed to parse expression "${expr}": ${message}`);
253
+ }
254
+ }
255
+ export function expressionUsesVariable(expr, varName) {
256
+ try {
257
+ const ast = parse(expr, { sourceType: 'module' });
258
+ let found = false;
259
+ traverse(ast, {
260
+ Identifier(path) {
261
+ if (path.node.name === varName) {
262
+ if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
263
+ return;
264
+ }
265
+ found = true;
266
+ path.stop();
267
+ }
268
+ }
269
+ });
270
+ return found;
271
+ }
272
+ catch (e) {
273
+ const message = e instanceof Error ? e.message : String(e);
274
+ throw new Error(`Failed to parse expression "${expr}": ${message}`);
275
+ }
276
+ }
package/Generator.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ interface GeneratorOptions {
2
+ appName: string;
3
+ outputDir: string;
4
+ }
5
+ export declare class Generator {
6
+ generate(options: GeneratorOptions): void;
7
+ private writeFile;
8
+ private toKebabCase;
9
+ private toPascalCase;
10
+ private toTitleCase;
11
+ private getPackageJson;
12
+ private getTsConfig;
13
+ private getFluffJson;
14
+ private getIndexHtml;
15
+ private getMainTs;
16
+ private getComponentTs;
17
+ private getComponentHtml;
18
+ private getComponentCss;
19
+ }
20
+ export {};
21
+ //# sourceMappingURL=Generator.d.ts.map