@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,330 @@
1
+ import * as parse5 from 'parse5';
2
+ import { html as parse5Html } from 'parse5';
3
+ import { ControlFlowParser } from './ControlFlowParser.js';
4
+ import { expressionUsesVariable, parseInterpolations, transformInterpolation } from './ExpressionTransformer.js';
5
+ export class TemplateParser {
6
+ bindingId = 0;
7
+ bindings = [];
8
+ controlFlows = [];
9
+ controlFlowParser = new ControlFlowParser();
10
+ templateRefs = new Set();
11
+ parse(html) {
12
+ this.bindingId = 0;
13
+ this.bindings = [];
14
+ this.controlFlows = [];
15
+ this.templateRefs = new Set();
16
+ let result = html;
17
+ result = this.processControlFlowBlocks(result);
18
+ result = this.processWithParse5(result);
19
+ return {
20
+ html: result,
21
+ bindings: this.bindings,
22
+ controlFlows: this.controlFlows,
23
+ templateRefs: Array.from(this.templateRefs)
24
+ };
25
+ }
26
+ processControlFlowBlocks(html) {
27
+ let result = this.processSwitchBlocksNew(html);
28
+ result = this.processForBlocksNew(result);
29
+ result = this.processIfBlocksNew(result);
30
+ return result;
31
+ }
32
+ processSwitchBlocksNew(html) {
33
+ const { result: parsed, blocks } = this.controlFlowParser.parseSwitchBlocks(html);
34
+ let result = parsed;
35
+ for (let i = 0; i < blocks.length; i++) {
36
+ const block = blocks[i];
37
+ if (!block)
38
+ continue;
39
+ const id = `l${this.bindingId++}`;
40
+ const processedCases = block.cases.map(c => ({
41
+ value: c.value,
42
+ content: this.processControlFlowBlocks(this.processControlFlowContent(c.content))
43
+ .trim(),
44
+ fallthrough: c.fallthrough
45
+ }));
46
+ this.controlFlows.push({
47
+ id, type: 'switch', expression: block.expression, cases: processedCases
48
+ });
49
+ const placeholder = `__SWITCH_BLOCK_${i}__`;
50
+ const replacement = `<!--${id}--><!--/${id}-->`;
51
+ result = result.replace(placeholder, replacement);
52
+ }
53
+ return result;
54
+ }
55
+ processForBlocksNew(html) {
56
+ const { result: parsed, blocks } = this.controlFlowParser.parseForBlocks(html);
57
+ let result = parsed;
58
+ for (let i = 0; i < blocks.length; i++) {
59
+ const block = blocks[i];
60
+ if (!block)
61
+ continue;
62
+ const id = `l${this.bindingId++}`;
63
+ let content = this.processControlFlowBlocks(block.content);
64
+ content = this.processControlFlowContent(content, block.iterator);
65
+ const trimmedContent = content.trim();
66
+ const isOptionContent = trimmedContent.startsWith('<option');
67
+ this.controlFlows.push({
68
+ id,
69
+ type: 'for',
70
+ iterator: block.iterator,
71
+ iterable: block.iterable,
72
+ trackBy: block.trackBy,
73
+ content: trimmedContent
74
+ });
75
+ const placeholder = `__FOR_BLOCK_${i}__`;
76
+ const replacement = isOptionContent ? `<!--for-${id}--><!--/for-${id}-->` : `<!--${id}--><!--/${id}-->`;
77
+ result = result.replace(placeholder, replacement);
78
+ }
79
+ return result;
80
+ }
81
+ processIfBlocksNew(html) {
82
+ const { result: parsed, blocks } = this.controlFlowParser.parseIfBlocks(html);
83
+ let result = parsed;
84
+ for (let i = 0; i < blocks.length; i++) {
85
+ const block = blocks[i];
86
+ if (!block)
87
+ continue;
88
+ const id = `l${this.bindingId++}`;
89
+ let ifContent = this.processControlFlowBlocks(block.ifContent);
90
+ ifContent = this.processControlFlowContent(ifContent);
91
+ let elseContent = '';
92
+ if (block.elseContent) {
93
+ elseContent = this.processControlFlowBlocks(block.elseContent);
94
+ elseContent = this.processControlFlowContent(elseContent);
95
+ }
96
+ this.controlFlows.push({
97
+ id, type: 'if', condition: block.condition, ifContent: ifContent.trim(), elseContent: elseContent.trim()
98
+ });
99
+ const placeholder = `__IF_BLOCK_${i}__`;
100
+ const replacement = `<!--${id}--><!--/${id}-->`;
101
+ result = result.replace(placeholder, replacement);
102
+ }
103
+ return result;
104
+ }
105
+ processWithParse5(html) {
106
+ const fragment = parse5.parseFragment(html, { sourceCodeLocationInfo: true });
107
+ this.walkNodes(fragment.childNodes, html);
108
+ return parse5.serialize(fragment);
109
+ }
110
+ walkNodes(nodes, source) {
111
+ for (let i = 0; i < nodes.length; i++) {
112
+ const node = nodes[i];
113
+ if (this.isElement(node)) {
114
+ this.processElement(node, source);
115
+ if (node.childNodes) {
116
+ this.walkNodes(node.childNodes, source);
117
+ }
118
+ }
119
+ else if (this.isTextNode(node)) {
120
+ const newNodes = this.processTextNode(node);
121
+ if (newNodes.length > 0) {
122
+ nodes.splice(i, 1, ...newNodes);
123
+ i += newNodes.length - 1;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ isTextNode(node) {
129
+ return 'value' in node && !('tagName' in node);
130
+ }
131
+ processTextNode(textNode, iteratorVar, inControlFlow = false) {
132
+ const text = textNode.value;
133
+ const interpolations = parseInterpolations(text);
134
+ if (interpolations.length === 0) {
135
+ return [textNode];
136
+ }
137
+ const nodes = [];
138
+ let lastIndex = 0;
139
+ for (const { start, end, expr } of interpolations) {
140
+ if (start > lastIndex) {
141
+ nodes.push(this.createTextNode(text.slice(lastIndex, start)));
142
+ }
143
+ if (inControlFlow) {
144
+ const usesIterator = iteratorVar && expressionUsesVariable(expr, iteratorVar);
145
+ const transformed = transformInterpolation(expr, iteratorVar);
146
+ if (usesIterator) {
147
+ nodes.push(this.createTextNode(`\${${transformed}}`));
148
+ }
149
+ else {
150
+ const id = `l${this.bindingId++}`;
151
+ nodes.push(this.createSpanWithTextBind(transformed, id));
152
+ }
153
+ }
154
+ else {
155
+ const id = `l${this.bindingId++}`;
156
+ this.bindings.push({
157
+ id, type: 'text', expression: expr
158
+ });
159
+ nodes.push(this.createSpanElement(id));
160
+ }
161
+ lastIndex = end;
162
+ }
163
+ if (lastIndex < text.length) {
164
+ nodes.push(this.createTextNode(text.slice(lastIndex)));
165
+ }
166
+ return nodes;
167
+ }
168
+ createTextNode(value) {
169
+ return {
170
+ nodeName: '#text',
171
+ value,
172
+ parentNode: null
173
+ };
174
+ }
175
+ createSpanElement(id) {
176
+ return {
177
+ nodeName: 'span',
178
+ tagName: 'span',
179
+ attrs: [{ name: 'data-lid', value: id }],
180
+ childNodes: [],
181
+ namespaceURI: parse5Html.NS.HTML,
182
+ parentNode: null
183
+ };
184
+ }
185
+ isElement(node) {
186
+ return 'tagName' in node;
187
+ }
188
+ getOriginalAttrName(element, attr, source) {
189
+ const attrLocations = 'sourceCodeLocation' in element && element.sourceCodeLocation &&
190
+ typeof element.sourceCodeLocation === 'object' && 'attrs' in element.sourceCodeLocation
191
+ ? element.sourceCodeLocation.attrs
192
+ : undefined;
193
+ if (attrLocations?.[attr.name]) {
194
+ const loc = attrLocations[attr.name];
195
+ if (loc) {
196
+ const [originalName] = source.slice(loc.startOffset, loc.endOffset)
197
+ .split('=');
198
+ return originalName;
199
+ }
200
+ }
201
+ return attr.name;
202
+ }
203
+ processElement(element, source) {
204
+ const bindings = [];
205
+ const attrsToRemove = [];
206
+ for (const attr of element.attrs) {
207
+ if (attr.name.startsWith('[(') && attr.name.endsWith(')]')) {
208
+ const prop = attr.name.slice(2, -2);
209
+ const expr = attr.value;
210
+ bindings.push({ type: 'property', target: prop, expression: expr });
211
+ bindings.push({ type: 'event', eventName: 'input', expression: `${expr} = $event.target.${prop}` });
212
+ attrsToRemove.push(attr.name);
213
+ }
214
+ else if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
215
+ const prop = attr.name.slice(1, -1);
216
+ const expr = attr.value;
217
+ if (prop.startsWith('class.')) {
218
+ bindings.push({ type: 'class', className: prop.slice(6), expression: expr });
219
+ }
220
+ else if (prop.startsWith('style.')) {
221
+ bindings.push({ type: 'style', styleProp: prop.slice(6), expression: expr });
222
+ }
223
+ else {
224
+ const dangerousProps = ['innerHTML', 'outerHTML', 'href', 'src'];
225
+ const isUnsafe = prop.endsWith('.unsafe');
226
+ const baseProp = isUnsafe ? prop.slice(0, -7) : prop;
227
+ if (dangerousProps.includes(baseProp) && !isUnsafe) {
228
+ throw new Error(`XSS Protection: [${prop}] binding is blocked. ` + `Use [${prop}.unsafe] if you understand the security implications ` + 'and have sanitized user input.');
229
+ }
230
+ bindings.push({ type: 'property', target: baseProp, expression: expr });
231
+ }
232
+ attrsToRemove.push(attr.name);
233
+ }
234
+ else if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
235
+ const eventName = attr.name.slice(1, -1);
236
+ bindings.push({ type: 'event', eventName, expression: attr.value });
237
+ attrsToRemove.push(attr.name);
238
+ }
239
+ else if (attr.name.startsWith('#')) {
240
+ const originalName = this.getOriginalAttrName(element, attr, source);
241
+ const refName = originalName.slice(1);
242
+ attrsToRemove.push(attr.name);
243
+ element.attrs.push({ name: 'data-ref', value: refName });
244
+ this.templateRefs.add(refName);
245
+ }
246
+ }
247
+ if (bindings.length > 0) {
248
+ const id = `l${this.bindingId++}`;
249
+ element.attrs = element.attrs.filter(a => !attrsToRemove.includes(a.name));
250
+ element.attrs.push({ name: 'data-lid', value: id });
251
+ for (const binding of bindings) {
252
+ this.bindings.push({ ...binding, id });
253
+ }
254
+ }
255
+ }
256
+ processControlFlowContent(html, iteratorVar) {
257
+ const fragment = parse5.parseFragment(html, { sourceCodeLocationInfo: true });
258
+ this.walkControlFlowNodes(fragment.childNodes, html, iteratorVar);
259
+ return parse5.serialize(fragment);
260
+ }
261
+ walkControlFlowNodes(nodes, source, iteratorVar) {
262
+ for (let i = 0; i < nodes.length; i++) {
263
+ const node = nodes[i];
264
+ if (this.isElement(node)) {
265
+ this.processControlFlowElement(node, source, iteratorVar);
266
+ if (node.childNodes) {
267
+ this.walkControlFlowNodes(node.childNodes, source, iteratorVar);
268
+ }
269
+ }
270
+ else if (this.isTextNode(node)) {
271
+ const newNodes = this.processTextNode(node, iteratorVar, true);
272
+ if (newNodes.length > 0) {
273
+ nodes.splice(i, 1, ...newNodes);
274
+ i += newNodes.length - 1;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ processControlFlowElement(element, source, iteratorVar) {
280
+ for (const attr of element.attrs) {
281
+ const originalName = this.getOriginalAttrName(element, attr, source);
282
+ if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
283
+ const lowercaseProp = attr.name.slice(1, -1);
284
+ const originalProp = originalName.slice(1, -1); // Remove [ ]
285
+ if (originalProp !== lowercaseProp) {
286
+ attr.value = JSON.stringify([originalProp, attr.value]);
287
+ }
288
+ }
289
+ else if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
290
+ const lowercaseEvent = attr.name.slice(1, -1);
291
+ const originalEvent = originalName.slice(1, -1); // Remove ( )
292
+ if (originalEvent !== lowercaseEvent) {
293
+ attr.value = JSON.stringify([originalEvent, attr.value]);
294
+ }
295
+ }
296
+ else if (attr.name.startsWith('#')) {
297
+ const refName = originalName.slice(1);
298
+ attr.name = 'data-ref';
299
+ attr.value = refName;
300
+ this.templateRefs.add(refName);
301
+ }
302
+ const interpolations = parseInterpolations(attr.value);
303
+ if (interpolations.length > 0) {
304
+ let newValue = '';
305
+ let lastEnd = 0;
306
+ for (const { start, end, expr } of interpolations) {
307
+ newValue += attr.value.slice(lastEnd, start);
308
+ const transformed = transformInterpolation(expr, iteratorVar);
309
+ newValue += `\${${transformed}}`;
310
+ lastEnd = end;
311
+ }
312
+ newValue += attr.value.slice(lastEnd);
313
+ attr.value = newValue;
314
+ }
315
+ }
316
+ }
317
+ createSpanWithTextBind(expr, id) {
318
+ return {
319
+ nodeName: 'span',
320
+ tagName: 'span',
321
+ attrs: [
322
+ { name: 'data-text-bind', value: expr },
323
+ { name: 'data-lid', value: id }
324
+ ],
325
+ childNodes: [],
326
+ namespaceURI: parse5Html.NS.HTML,
327
+ parentNode: null
328
+ };
329
+ }
330
+ }
@@ -0,0 +1,20 @@
1
+ import type { PluginObj } from '@babel/core';
2
+ export interface ClassTransformOptions {
3
+ className: string;
4
+ originalSuperClass?: string;
5
+ newSuperClass?: string;
6
+ injectMethods?: {
7
+ name: string;
8
+ body: string;
9
+ }[];
10
+ }
11
+ interface PluginState {
12
+ opts: ClassTransformOptions;
13
+ }
14
+ export default function classTransformPlugin(): PluginObj<PluginState>;
15
+ export declare function injectMethodBodies(code: string, methods: {
16
+ name: string;
17
+ body: string;
18
+ }[]): string;
19
+ export {};
20
+ //# sourceMappingURL=babel-plugin-class-transform.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { types as t } from '@babel/core';
2
+ export default function classTransformPlugin() {
3
+ return {
4
+ name: 'babel-plugin-class-transform', visitor: {
5
+ ClassDeclaration(path, state) {
6
+ const opts = state.opts || {};
7
+ const { className, originalSuperClass, newSuperClass, injectMethods } = opts;
8
+ if (!path.node.id?.name || path.node.id.name !== className) {
9
+ return;
10
+ }
11
+ if (originalSuperClass && newSuperClass && path.node.superClass) {
12
+ if (t.isIdentifier(path.node.superClass) && path.node.superClass.name === originalSuperClass) {
13
+ path.node.superClass = t.identifier(newSuperClass);
14
+ }
15
+ }
16
+ if (injectMethods && injectMethods.length > 0) {
17
+ const newMethods = [];
18
+ for (const method of injectMethods) {
19
+ const methodNode = t.classMethod('method', t.identifier(method.name), [], t.blockStatement([
20
+ t.expressionStatement(t.identifier(`__INJECT_${method.name}__`))
21
+ ]));
22
+ newMethods.push(methodNode);
23
+ }
24
+ for (const method of newMethods.reverse()) {
25
+ path.get('body')
26
+ .unshiftContainer('body', method);
27
+ }
28
+ }
29
+ }
30
+ }
31
+ };
32
+ }
33
+ export function injectMethodBodies(code, methods) {
34
+ let result = code;
35
+ for (const method of methods) {
36
+ const placeholder = `__INJECT_${method.name}__;`;
37
+ result = result.replace(placeholder, method.body);
38
+ }
39
+ return result;
40
+ }
@@ -0,0 +1,14 @@
1
+ import type { PluginObj } from '@babel/core';
2
+ export interface ComponentMetadata {
3
+ selector: string;
4
+ templateUrl: string;
5
+ styleUrl?: string;
6
+ className: string;
7
+ }
8
+ export declare const componentMetadataMap: Map<string, ComponentMetadata>;
9
+ interface PluginState {
10
+ filename?: string;
11
+ }
12
+ export default function componentPlugin(): PluginObj<PluginState>;
13
+ export {};
14
+ //# sourceMappingURL=babel-plugin-component.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { types as t } from '@babel/core';
2
+ export const componentMetadataMap = new Map();
3
+ export default function componentPlugin() {
4
+ return {
5
+ name: 'babel-plugin-component', visitor: {
6
+ ClassDeclaration(path, state) {
7
+ const decorators = path.node.decorators ?? [];
8
+ const componentDecorator = decorators.find(dec => {
9
+ if (t.isCallExpression(dec.expression)) {
10
+ const { callee } = dec.expression;
11
+ return t.isIdentifier(callee) && callee.name === 'Component';
12
+ }
13
+ return false;
14
+ });
15
+ if (!componentDecorator)
16
+ return;
17
+ if (!t.isCallExpression(componentDecorator.expression))
18
+ return;
19
+ const args = componentDecorator.expression.arguments;
20
+ if (args.length === 0)
21
+ return;
22
+ const [configArg] = args;
23
+ if (!t.isObjectExpression(configArg))
24
+ return;
25
+ const metadata = {};
26
+ if (path.node.id) {
27
+ metadata.className = path.node.id.name;
28
+ }
29
+ for (const prop of configArg.properties) {
30
+ if (!t.isObjectProperty(prop))
31
+ continue;
32
+ if (!t.isIdentifier(prop.key))
33
+ continue;
34
+ const key = prop.key.name;
35
+ const { value } = prop;
36
+ if (t.isStringLiteral(value)) {
37
+ switch (key) {
38
+ case 'selector':
39
+ metadata.selector = value.value;
40
+ break;
41
+ case 'templateUrl':
42
+ metadata.templateUrl = value.value;
43
+ break;
44
+ case 'styleUrl':
45
+ metadata.styleUrl = value.value;
46
+ break;
47
+ }
48
+ }
49
+ else if (t.isTemplateLiteral(value) && value.quasis.length === 1) {
50
+ const strValue = value.quasis[0].value.cooked ?? value.quasis[0].value.raw;
51
+ switch (key) {
52
+ case 'selector':
53
+ metadata.selector = strValue;
54
+ break;
55
+ case 'templateUrl':
56
+ metadata.templateUrl = strValue;
57
+ break;
58
+ case 'styleUrl':
59
+ metadata.styleUrl = strValue;
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ const filename = state.filename ?? 'unknown';
65
+ if (metadata.selector && metadata.templateUrl && metadata.className) {
66
+ componentMetadataMap.set(filename, {
67
+ selector: metadata.selector,
68
+ templateUrl: metadata.templateUrl,
69
+ className: metadata.className,
70
+ styleUrl: metadata.styleUrl
71
+ });
72
+ }
73
+ }
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,13 @@
1
+ import type { PluginObj } from '@babel/core';
2
+ export interface ImportTransformOptions {
3
+ removeImportsFrom?: string[];
4
+ removeDecorators?: string[];
5
+ pathReplacements?: Record<string, string>;
6
+ addJsExtension?: boolean;
7
+ }
8
+ interface PluginState {
9
+ opts: ImportTransformOptions;
10
+ }
11
+ export default function importsPlugin(): PluginObj<PluginState>;
12
+ export {};
13
+ //# sourceMappingURL=babel-plugin-imports.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { types as t } from '@babel/core';
2
+ function getDecoratorName(decorator) {
3
+ const expr = decorator.expression;
4
+ if (t.isCallExpression(expr) && t.isIdentifier(expr.callee)) {
5
+ return expr.callee.name;
6
+ }
7
+ if (t.isIdentifier(expr)) {
8
+ return expr.name;
9
+ }
10
+ return null;
11
+ }
12
+ export default function importsPlugin() {
13
+ return {
14
+ name: 'babel-plugin-imports', visitor: {
15
+ ImportDeclaration(path, state) {
16
+ const opts = state.opts ?? {};
17
+ const source = path.node.source.value;
18
+ if (opts.removeImportsFrom?.includes(source)) {
19
+ path.remove();
20
+ return;
21
+ }
22
+ let newSource = source;
23
+ if (opts.pathReplacements) {
24
+ for (const [from, to] of Object.entries(opts.pathReplacements)) {
25
+ if (newSource.startsWith(from)) {
26
+ newSource = to + newSource.slice(from.length);
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ if (opts.addJsExtension && newSource.startsWith('.') && !newSource.endsWith('.js')) {
32
+ newSource = newSource + '.js';
33
+ }
34
+ if (newSource !== source) {
35
+ path.node.source = t.stringLiteral(newSource);
36
+ }
37
+ },
38
+ ClassDeclaration(path, state) {
39
+ const { removeDecorators } = state.opts ?? {};
40
+ if (!removeDecorators?.length)
41
+ return;
42
+ const { decorators } = path.node;
43
+ if (!decorators)
44
+ return;
45
+ path.node.decorators = decorators.filter(dec => {
46
+ const name = getDecoratorName(dec);
47
+ return !name || !removeDecorators.includes(name);
48
+ });
49
+ },
50
+ ClassProperty(path, state) {
51
+ const { removeDecorators } = state.opts ?? {};
52
+ if (!removeDecorators?.length)
53
+ return;
54
+ const { decorators } = path.node;
55
+ if (!decorators)
56
+ return;
57
+ path.node.decorators = decorators.filter(dec => {
58
+ const name = getDecoratorName(dec);
59
+ return !name || !removeDecorators.includes(name);
60
+ });
61
+ },
62
+ ClassMethod(path, state) {
63
+ const { removeDecorators } = state.opts ?? {};
64
+ if (!removeDecorators?.length)
65
+ return;
66
+ const { decorators } = path.node;
67
+ if (!decorators)
68
+ return;
69
+ path.node.decorators = decorators.filter(dec => {
70
+ const name = getDecoratorName(dec);
71
+ return !name || !removeDecorators.includes(name);
72
+ });
73
+ }
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,22 @@
1
+ import type { PluginObj } from '@babel/core';
2
+ import { types as t } from '@babel/core';
3
+ interface WatchInfo {
4
+ methodName: string;
5
+ watchedProps: string[];
6
+ }
7
+ interface WatchCallInfo {
8
+ propName: string;
9
+ watchedProps: string[];
10
+ callbackArg: t.Expression | t.SpreadElement | t.JSXNamespacedName | t.ArgumentPlaceholder;
11
+ }
12
+ interface PluginState {
13
+ filename?: string;
14
+ needsPropertyImport?: boolean;
15
+ reactiveProperties?: Set<string>;
16
+ watchMethods?: WatchInfo[];
17
+ watchCalls?: WatchCallInfo[];
18
+ }
19
+ export declare const reactivePropertiesMap: Map<string, Set<string>>;
20
+ export default function reactivePlugin(): PluginObj<PluginState>;
21
+ export {};
22
+ //# sourceMappingURL=babel-plugin-reactive.d.ts.map