@fluffjs/cli 0.3.9 → 0.4.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.
@@ -11,6 +11,36 @@ export type { IfMarkerConfig } from './interfaces/IfMarkerConfig.js';
11
11
  export type { SwitchMarkerConfig } from './interfaces/SwitchMarkerConfig.js';
12
12
  export type { TextMarkerConfig } from './interfaces/TextMarkerConfig.js';
13
13
  export type MarkerConfig = IfMarkerConfig | ForMarkerConfig | SwitchMarkerConfig | TextMarkerConfig | BreakMarkerConfig;
14
+ /**
15
+ * Compact Binding Format (Encoder)
16
+ *
17
+ * Bindings are serialized as tuples to minimize bundle size. All strings are
18
+ * interned into a global string table and referenced by index.
19
+ *
20
+ * Format: [nameIdx, bindType, deps, id, extras?]
21
+ *
22
+ * - nameIdx: Index into global string table for the binding name (e.g., "value", "click")
23
+ * - bindType: Numeric binding type (0=property, 1=event, 2=two-way, 3=class, 4=style, 5=ref)
24
+ * - deps: Array of interned dependency chains, or null. Each dep is either:
25
+ * - A single index (for simple property like "foo")
26
+ * - An array of indices (for nested property chain like ["device", "name"])
27
+ * - id: Expression ID (for property/two-way/class/style) or Handler ID (for event), or null
28
+ * - extras: Optional object with additional binding metadata:
29
+ * - t: Target property name for two-way bindings
30
+ * - s: Subscribe source name
31
+ * - p: Pipes array of [pipeNameIdx, argExprIds[]]
32
+ *
33
+ * The global string table is passed to FluffBase.__setExpressionTable() as the third argument.
34
+ * The runtime decodes these compact bindings back to BindingInfo objects.
35
+ */
36
+ export type CompactDep = number | number[];
37
+ export type CompactBinding = [
38
+ number,
39
+ number,
40
+ CompactDep[] | null,
41
+ number | null,
42
+ Record<string, unknown>?
43
+ ];
14
44
  export declare class CodeGenerator {
15
45
  private readonly componentSelectors;
16
46
  private readonly componentSelector;
@@ -18,6 +48,8 @@ export declare class CodeGenerator {
18
48
  private static globalExprs;
19
49
  private static readonly globalHandlerIdsByExpr;
20
50
  private static globalHandlers;
51
+ private static readonly globalStringTable;
52
+ private static readonly globalStringIndices;
21
53
  private markerId;
22
54
  private readonly markerConfigs;
23
55
  private readonly usedExprIds;
@@ -27,16 +59,18 @@ export declare class CodeGenerator {
27
59
  private readonly collectedTemplates;
28
60
  constructor(componentSelectors?: Set<string>, componentSelector?: string);
29
61
  static resetGlobalState(): void;
62
+ private static internString;
63
+ static getStringTable(): string[];
30
64
  generateRenderMethod(template: ParsedTemplate, styles?: string): string;
31
65
  generateHtml(template: ParsedTemplate): string;
32
66
  generateRenderMethodFromHtml(html: string, styles?: string, markerConfigExpr?: t.Expression): string;
33
67
  getMarkerConfigExpression(): t.Expression;
34
68
  private buildMarkerConfigExpression;
35
- private buildMarkerConfigObject;
36
- private buildDepsExpression;
37
- private buildPropertyChainExpression;
69
+ private buildMarkerConfigArray;
70
+ private buildCompactDepsExpression;
71
+ private buildCompactPropertyChainExpression;
38
72
  generateBindingsSetup(): string;
39
- getBindingsMap(): Record<string, Record<string, unknown>[]>;
73
+ getBindingsMap(): Record<string, CompactBinding[]>;
40
74
  generateExpressionAssignments(): string;
41
75
  static generateGlobalExprTable(): string;
42
76
  private static buildExpressionArrowFunction;
@@ -49,6 +83,7 @@ export declare class CodeGenerator {
49
83
  private renderTextToParent;
50
84
  private isComponentTag;
51
85
  private serializeBinding;
86
+ private internDep;
52
87
  private internExpression;
53
88
  private internHandler;
54
89
  private renderInterpolationToParent;
package/CodeGenerator.js CHANGED
@@ -5,6 +5,20 @@ import { generate, parseMethodBody } from './BabelHelpers.js';
5
5
  import { ExpressionTransformer } from './ExpressionTransformer.js';
6
6
  import { Parse5Helpers } from './Parse5Helpers.js';
7
7
  const RESTRICTED_ELEMENT_PREFIX = 'x-fluff-el-';
8
+ const BIND_PROPERTY = 0;
9
+ const BIND_EVENT = 1;
10
+ const BIND_TWO_WAY = 2;
11
+ const BIND_CLASS = 3;
12
+ const BIND_STYLE = 4;
13
+ const BIND_REF = 5;
14
+ const BINDING_TYPE_MAP = {
15
+ 'property': BIND_PROPERTY,
16
+ 'event': BIND_EVENT,
17
+ 'two-way': BIND_TWO_WAY,
18
+ 'class': BIND_CLASS,
19
+ 'style': BIND_STYLE,
20
+ 'ref': BIND_REF
21
+ };
8
22
  export class CodeGenerator {
9
23
  componentSelectors;
10
24
  componentSelector;
@@ -12,6 +26,8 @@ export class CodeGenerator {
12
26
  static globalExprs = [];
13
27
  static globalHandlerIdsByExpr = new Map();
14
28
  static globalHandlers = [];
29
+ static globalStringTable = [];
30
+ static globalStringIndices = new Map();
15
31
  markerId = 0;
16
32
  markerConfigs = new Map();
17
33
  usedExprIds = [];
@@ -28,6 +44,21 @@ export class CodeGenerator {
28
44
  CodeGenerator.globalExprs = [];
29
45
  CodeGenerator.globalHandlerIdsByExpr.clear();
30
46
  CodeGenerator.globalHandlers = [];
47
+ CodeGenerator.globalStringTable.length = 0;
48
+ CodeGenerator.globalStringIndices.clear();
49
+ }
50
+ static internString(str) {
51
+ const existing = CodeGenerator.globalStringIndices.get(str);
52
+ if (existing !== undefined) {
53
+ return existing;
54
+ }
55
+ const id = CodeGenerator.globalStringTable.length;
56
+ CodeGenerator.globalStringTable.push(str);
57
+ CodeGenerator.globalStringIndices.set(str, id);
58
+ return id;
59
+ }
60
+ static getStringTable() {
61
+ return CodeGenerator.globalStringTable;
31
62
  }
32
63
  generateRenderMethod(template, styles) {
33
64
  this.markerId = 0;
@@ -71,73 +102,81 @@ export class CodeGenerator {
71
102
  const entries = Array.from(this.markerConfigs.entries())
72
103
  .map(([id, config]) => t.arrayExpression([
73
104
  t.numericLiteral(id),
74
- this.buildMarkerConfigObject(config)
105
+ this.buildMarkerConfigArray(config)
75
106
  ]));
76
107
  return t.arrayExpression(entries);
77
108
  }
78
- buildMarkerConfigObject(config) {
79
- const properties = [
80
- t.objectProperty(t.stringLiteral('type'), t.stringLiteral(config.type))
81
- ];
109
+ buildMarkerConfigArray(config) {
110
+ const MARKER_TYPE_MAP = {
111
+ 'if': 0, 'for': 1, 'text': 2, 'switch': 3, 'break': 4
112
+ };
113
+ const typeNum = MARKER_TYPE_MAP[config.type];
82
114
  if (config.type === 'text') {
83
- properties.push(t.objectProperty(t.stringLiteral('exprId'), t.numericLiteral(config.exprId)));
84
- if (config.deps) {
85
- properties.push(t.objectProperty(t.stringLiteral('deps'), this.buildDepsExpression(config.deps)));
86
- }
87
- if (config.pipes && config.pipes.length > 0) {
88
- properties.push(t.objectProperty(t.stringLiteral('pipes'), t.arrayExpression(config.pipes.map(pipe => t.objectExpression([
89
- t.objectProperty(t.stringLiteral('name'), t.stringLiteral(pipe.name)),
90
- t.objectProperty(t.stringLiteral('argExprIds'), t.arrayExpression(pipe.argExprIds.map(arg => t.numericLiteral(arg))))
91
- ])))));
92
- }
115
+ const elements = [
116
+ t.numericLiteral(typeNum),
117
+ t.numericLiteral(config.exprId),
118
+ config.deps ? this.buildCompactDepsExpression(config.deps) : t.nullLiteral(),
119
+ config.pipes && config.pipes.length > 0
120
+ ? t.arrayExpression(config.pipes.map(pipe => t.arrayExpression([
121
+ t.numericLiteral(CodeGenerator.internString(pipe.name)),
122
+ t.arrayExpression(pipe.argExprIds.map(arg => t.numericLiteral(arg)))
123
+ ])))
124
+ : t.nullLiteral()
125
+ ];
126
+ return t.arrayExpression(elements);
93
127
  }
94
128
  else if (config.type === 'if') {
95
- properties.push(t.objectProperty(t.stringLiteral('branches'), t.arrayExpression(config.branches.map(branch => {
96
- const branchProps = [];
97
- if (branch.exprId !== undefined) {
98
- branchProps.push(t.objectProperty(t.stringLiteral('exprId'), t.numericLiteral(branch.exprId)));
129
+ const branches = t.arrayExpression(config.branches.map(branch => {
130
+ if (branch.exprId === undefined && !branch.deps) {
131
+ return t.arrayExpression([]);
99
132
  }
100
- if (branch.deps) {
101
- branchProps.push(t.objectProperty(t.stringLiteral('deps'), this.buildDepsExpression(branch.deps)));
102
- }
103
- return t.objectExpression(branchProps);
104
- }))));
133
+ return t.arrayExpression([
134
+ branch.exprId !== undefined ? t.numericLiteral(branch.exprId) : t.nullLiteral(),
135
+ branch.deps ? this.buildCompactDepsExpression(branch.deps) : t.nullLiteral()
136
+ ]);
137
+ }));
138
+ return t.arrayExpression([t.numericLiteral(typeNum), branches]);
105
139
  }
106
140
  else if (config.type === 'for') {
107
- properties.push(t.objectProperty(t.stringLiteral('iterator'), t.stringLiteral(config.iterator)), t.objectProperty(t.stringLiteral('iterableExprId'), t.numericLiteral(config.iterableExprId)), t.objectProperty(t.stringLiteral('hasEmpty'), t.booleanLiteral(config.hasEmpty)));
108
- if (config.deps) {
109
- properties.push(t.objectProperty(t.stringLiteral('deps'), this.buildDepsExpression(config.deps)));
110
- }
111
- if (config.trackBy !== undefined) {
112
- properties.push(t.objectProperty(t.stringLiteral('trackBy'), t.stringLiteral(config.trackBy)));
113
- }
141
+ return t.arrayExpression([
142
+ t.numericLiteral(typeNum),
143
+ t.numericLiteral(CodeGenerator.internString(config.iterator)),
144
+ t.numericLiteral(config.iterableExprId),
145
+ t.booleanLiteral(config.hasEmpty),
146
+ config.deps ? this.buildCompactDepsExpression(config.deps) : t.nullLiteral(),
147
+ config.trackBy !== undefined
148
+ ? t.numericLiteral(CodeGenerator.internString(config.trackBy))
149
+ : t.nullLiteral()
150
+ ]);
114
151
  }
115
152
  else if (config.type === 'switch') {
116
- properties.push(t.objectProperty(t.stringLiteral('expressionExprId'), t.numericLiteral(config.expressionExprId)));
117
- if (config.deps) {
118
- properties.push(t.objectProperty(t.stringLiteral('deps'), this.buildDepsExpression(config.deps)));
119
- }
120
- properties.push(t.objectProperty(t.stringLiteral('cases'), t.arrayExpression(config.cases.map(caseConfig => {
121
- const caseProps = [
122
- t.objectProperty(t.stringLiteral('isDefault'), t.booleanLiteral(caseConfig.isDefault)),
123
- t.objectProperty(t.stringLiteral('fallthrough'), t.booleanLiteral(caseConfig.fallthrough))
124
- ];
125
- if (caseConfig.valueExprId !== undefined) {
126
- caseProps.push(t.objectProperty(t.stringLiteral('valueExprId'), t.numericLiteral(caseConfig.valueExprId)));
127
- }
128
- return t.objectExpression(caseProps);
129
- }))));
130
- }
131
- return t.objectExpression(properties);
132
- }
133
- buildDepsExpression(deps) {
134
- return t.arrayExpression(deps.map(dep => this.buildPropertyChainExpression(dep)));
135
- }
136
- buildPropertyChainExpression(dep) {
153
+ const cases = t.arrayExpression(config.cases.map(caseConfig => t.arrayExpression([
154
+ t.booleanLiteral(caseConfig.isDefault),
155
+ t.booleanLiteral(caseConfig.fallthrough),
156
+ caseConfig.valueExprId !== undefined
157
+ ? t.numericLiteral(caseConfig.valueExprId)
158
+ : t.nullLiteral()
159
+ ])));
160
+ return t.arrayExpression([
161
+ t.numericLiteral(typeNum),
162
+ t.numericLiteral(config.expressionExprId),
163
+ config.deps ? this.buildCompactDepsExpression(config.deps) : t.nullLiteral(),
164
+ cases
165
+ ]);
166
+ }
167
+ else if (config.type === 'break') {
168
+ return t.arrayExpression([t.numericLiteral(typeNum)]);
169
+ }
170
+ return t.arrayExpression([t.numericLiteral(typeNum)]);
171
+ }
172
+ buildCompactDepsExpression(deps) {
173
+ return t.arrayExpression(deps.map(dep => this.buildCompactPropertyChainExpression(dep)));
174
+ }
175
+ buildCompactPropertyChainExpression(dep) {
137
176
  if (Array.isArray(dep)) {
138
- return t.arrayExpression(dep.map(part => t.stringLiteral(part)));
177
+ return t.arrayExpression(dep.map(part => t.numericLiteral(CodeGenerator.internString(part))));
139
178
  }
140
- return t.stringLiteral(dep);
179
+ return t.numericLiteral(CodeGenerator.internString(dep));
141
180
  }
142
181
  generateBindingsSetup() {
143
182
  const statements = [
@@ -179,8 +218,13 @@ export class CodeGenerator {
179
218
  const normalizedHandler = CodeGenerator.normalizeCompiledExpr(h);
180
219
  return CodeGenerator.buildHandlerArrowFunction(['t', 'l', '__ev'], normalizedHandler);
181
220
  });
221
+ const stringElements = CodeGenerator.globalStringTable.map(s => t.stringLiteral(s));
182
222
  const fluffBaseImport = t.importDeclaration([t.importSpecifier(t.identifier('FluffBase'), t.identifier('FluffBase'))], t.stringLiteral('@fluffjs/fluff'));
183
- const setExprTableCall = t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('FluffBase'), t.identifier('__setExpressionTable')), [t.arrayExpression(exprElements), t.arrayExpression(handlerElements)]));
223
+ const setExprTableCall = t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('FluffBase'), t.identifier('__setExpressionTable')), [
224
+ t.arrayExpression(exprElements),
225
+ t.arrayExpression(handlerElements),
226
+ t.arrayExpression(stringElements)
227
+ ]));
184
228
  const program = t.program([fluffBaseImport, setExprTableCall]);
185
229
  return generate(program, { compact: false }).code;
186
230
  }
@@ -284,46 +328,52 @@ export class CodeGenerator {
284
328
  return this.componentSelectors.has(resolvedTagName);
285
329
  }
286
330
  serializeBinding(binding) {
287
- const result = {
288
- n: binding.name,
289
- b: binding.binding
290
- };
331
+ const nameIdx = CodeGenerator.internString(binding.name);
332
+ const bindType = BINDING_TYPE_MAP[binding.binding];
291
333
  if (binding.binding === 'ref') {
292
- return result;
293
- }
294
- if (binding.deps) {
295
- result.d = binding.deps;
296
- }
297
- if (binding.subscribe) {
298
- result.s = binding.subscribe;
334
+ return [nameIdx, bindType, null, null];
299
335
  }
336
+ const deps = binding.deps
337
+ ? binding.deps.map(dep => this.internDep(dep))
338
+ : null;
339
+ let id = null;
300
340
  if (binding.binding === 'event') {
301
341
  if (!binding.expression) {
302
342
  throw new Error(`Event binding for ${binding.name} is missing expression`);
303
343
  }
304
- result.h = this.internHandler(binding.expression);
305
- return result;
344
+ id = this.internHandler(binding.expression);
345
+ return [nameIdx, bindType, deps, id];
306
346
  }
347
+ if (!binding.expression) {
348
+ throw new Error(`Binding for ${binding.name} is missing expression`);
349
+ }
350
+ id = this.internExpression(binding.expression);
351
+ const extras = {};
307
352
  if (binding.binding === 'two-way') {
308
- if (!binding.expression) {
309
- throw new Error(`Two-way binding for ${binding.name} is missing expression`);
310
- }
311
353
  if (!binding.expression.startsWith('this.')) {
312
354
  throw new Error(`Two-way binding for ${binding.name} must target a component property`);
313
355
  }
314
- result.t = binding.expression.slice('this.'.length);
356
+ extras.t = binding.expression.slice('this.'.length);
315
357
  }
316
- if (!binding.expression) {
317
- throw new Error(`Binding for ${binding.name} is missing expression`);
358
+ if (binding.subscribe) {
359
+ extras.s = binding.subscribe;
318
360
  }
319
- result.e = this.internExpression(binding.expression);
320
361
  if (binding.pipes && binding.pipes.length > 0) {
321
- result.p = binding.pipes.map(pipe => ({
322
- n: pipe.name,
323
- a: pipe.args.map(arg => this.internExpression(arg))
324
- }));
362
+ extras.p = binding.pipes.map(pipe => ([
363
+ CodeGenerator.internString(pipe.name),
364
+ pipe.args.map(arg => this.internExpression(arg))
365
+ ]));
325
366
  }
326
- return result;
367
+ if (Object.keys(extras).length > 0) {
368
+ return [nameIdx, bindType, deps, id, extras];
369
+ }
370
+ return [nameIdx, bindType, deps, id];
371
+ }
372
+ internDep(dep) {
373
+ if (Array.isArray(dep)) {
374
+ return dep.map(s => CodeGenerator.internString(s));
375
+ }
376
+ return CodeGenerator.internString(dep);
327
377
  }
328
378
  internExpression(expr) {
329
379
  const existing = CodeGenerator.globalExprIdsByExpr.get(expr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluffjs/cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -9,6 +9,9 @@ type MarkerConfigEntriesLiteral = [number, MarkerConfigLiteral][];
9
9
  export declare class MarkerConfigAstReader {
10
10
  static readMarkerConfigEntries(code: string): MarkerConfigEntriesLiteral;
11
11
  static collectDeps(entries: MarkerConfigEntriesLiteral): string[];
12
+ private static collectDepsFromCompactConfig;
13
+ private static collectNumberDeps;
14
+ static collectCompactDeps(entries: MarkerConfigEntriesLiteral): number[][];
12
15
  private static collectDepsFromRecord;
13
16
  private static collectDepsFromIf;
14
17
  private static collectStringsFromDep;
@@ -31,6 +31,9 @@ export class MarkerConfigAstReader {
31
31
  static collectDeps(entries) {
32
32
  const deps = [];
33
33
  for (const [, config] of entries) {
34
+ if (Array.isArray(config)) {
35
+ continue;
36
+ }
34
37
  if (!MarkerConfigAstReader.isRecord(config)) {
35
38
  continue;
36
39
  }
@@ -56,6 +59,72 @@ export class MarkerConfigAstReader {
56
59
  }
57
60
  return deps;
58
61
  }
62
+ static collectDepsFromCompactConfig(config, deps) {
63
+ const [typeNum] = config;
64
+ if (typeof typeNum !== 'number')
65
+ return;
66
+ switch (typeNum) {
67
+ case 0: // if
68
+ {
69
+ const [, branches] = config;
70
+ if (Array.isArray(branches)) {
71
+ for (const branch of branches) {
72
+ if (Array.isArray(branch) && branch.length >= 2) {
73
+ const [, branchDeps] = branch;
74
+ if (Array.isArray(branchDeps)) {
75
+ MarkerConfigAstReader.collectNumberDeps(branchDeps, deps);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ break;
81
+ }
82
+ case 1: // for
83
+ {
84
+ const [, , , , forDeps] = config;
85
+ if (Array.isArray(forDeps)) {
86
+ MarkerConfigAstReader.collectNumberDeps(forDeps, deps);
87
+ }
88
+ break;
89
+ }
90
+ case 2: // text
91
+ {
92
+ const [, , textDeps] = config;
93
+ if (Array.isArray(textDeps)) {
94
+ MarkerConfigAstReader.collectNumberDeps(textDeps, deps);
95
+ }
96
+ break;
97
+ }
98
+ case 3: // switch
99
+ {
100
+ const [, , switchDeps] = config;
101
+ if (Array.isArray(switchDeps)) {
102
+ MarkerConfigAstReader.collectNumberDeps(switchDeps, deps);
103
+ }
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ static collectNumberDeps(compactDeps, deps) {
109
+ for (const dep of compactDeps) {
110
+ if (typeof dep === 'number') {
111
+ deps.push([dep]);
112
+ }
113
+ else if (Array.isArray(dep)) {
114
+ const numDeps = dep.filter((d) => typeof d === 'number');
115
+ deps.push(numDeps);
116
+ }
117
+ }
118
+ }
119
+ static collectCompactDeps(entries) {
120
+ const deps = [];
121
+ for (const [, config] of entries) {
122
+ if (Array.isArray(config)) {
123
+ MarkerConfigAstReader.collectDepsFromCompactConfig(config, deps);
124
+ }
125
+ }
126
+ return deps;
127
+ }
59
128
  static collectDepsFromRecord(configRecord, deps) {
60
129
  const configDeps = configRecord.deps;
61
130
  if (Array.isArray(configDeps)) {
@@ -153,7 +222,7 @@ export class MarkerConfigAstReader {
153
222
  return false;
154
223
  }
155
224
  const [id, config] = entry;
156
- return typeof id === 'number' && MarkerConfigAstReader.isRecord(config);
225
+ return typeof id === 'number' && (MarkerConfigAstReader.isRecord(config) || Array.isArray(config));
157
226
  });
158
227
  }
159
228
  static isRecord(value) {