@fluffjs/cli 0.2.4 → 0.3.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.
package/Cli.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare class Cli {
6
6
  private readonly noMinify;
7
7
  private readonly gzScriptTag;
8
8
  constructor(options?: CliOptions);
9
+ private resolveCwd;
9
10
  run(args: string[]): Promise<void>;
10
11
  private showHelp;
11
12
  private getProjectRoot;
@@ -16,6 +17,7 @@ export declare class Cli {
16
17
  private loadConfig;
17
18
  private loadConfigFrom;
18
19
  private tryResolveNxProject;
20
+ private findProjectJsonFiles;
19
21
  private init;
20
22
  private generate;
21
23
  private build;
package/Cli.js CHANGED
@@ -20,12 +20,19 @@ export class Cli {
20
20
  noMinify;
21
21
  gzScriptTag;
22
22
  constructor(options = {}) {
23
- this.cwd = options.cwd ?? process.env.INIT_CWD ?? process.cwd();
23
+ this.cwd = options.cwd ?? this.resolveCwd();
24
24
  this.nxPackage = options.nxPackage;
25
25
  this.noGzip = options.noGzip ?? false;
26
26
  this.noMinify = options.noMinify ?? false;
27
27
  this.gzScriptTag = options.gzScriptTag ?? false;
28
28
  }
29
+ resolveCwd() {
30
+ const processCwd = process.cwd();
31
+ if (fs.existsSync(path.join(processCwd, 'fluff.json'))) {
32
+ return processCwd;
33
+ }
34
+ return process.env.INIT_CWD ?? processCwd;
35
+ }
29
36
  async run(args) {
30
37
  const [command, ...commandArgs] = args;
31
38
  switch (command) {
@@ -151,6 +158,17 @@ Examples:
151
158
  return parsed;
152
159
  }
153
160
  tryResolveNxProject(nameOrDir, workspaceRoot) {
161
+ const projectJsonPaths = this.findProjectJsonFiles(workspaceRoot);
162
+ for (const projectJsonPath of projectJsonPaths) {
163
+ const projectDir = path.dirname(projectJsonPath);
164
+ const projectJsonContent = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
165
+ if (typeof projectJsonContent === 'object' && projectJsonContent !== null && 'name' in projectJsonContent) {
166
+ const projectName = projectJsonContent.name;
167
+ if (projectName === nameOrDir && fs.existsSync(path.join(projectDir, 'fluff.json'))) {
168
+ return projectDir;
169
+ }
170
+ }
171
+ }
154
172
  const packagesDir = path.join(workspaceRoot, 'packages');
155
173
  const appsDir = path.join(workspaceRoot, 'apps');
156
174
  const libsDir = path.join(workspaceRoot, 'libs');
@@ -182,6 +200,26 @@ Examples:
182
200
  }
183
201
  return null;
184
202
  }
203
+ findProjectJsonFiles(dir, depth = 0) {
204
+ if (depth > 3)
205
+ return [];
206
+ const results = [];
207
+ const projectJsonPath = path.join(dir, 'project.json');
208
+ if (fs.existsSync(projectJsonPath)) {
209
+ results.push(projectJsonPath);
210
+ }
211
+ if (depth < 3) {
212
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
213
+ for (const entry of entries) {
214
+ if (!entry.isDirectory())
215
+ continue;
216
+ if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.'))
217
+ continue;
218
+ results.push(...this.findProjectJsonFiles(path.join(dir, entry.name), depth + 1));
219
+ }
220
+ }
221
+ return results;
222
+ }
185
223
  init(args) {
186
224
  const [targetName] = args;
187
225
  const configPath = this.getConfigPath();
@@ -645,6 +683,7 @@ Examples:
645
683
  .replace(/\\/g, '/');
646
684
  return t.importDeclaration([], t.stringLiteral(relativePath));
647
685
  });
686
+ importDecls.unshift(t.importDeclaration([], t.stringLiteral('@fluff/expr-table')));
648
687
  const program = t.program(importDecls);
649
688
  const entryContent = generate(program, { compact: false }).code;
650
689
  return { contents: entryContent, resolveDir: srcDir };
@@ -1,3 +1,4 @@
1
+ import * as t from '@babel/types';
1
2
  import type { BreakMarkerConfig } from './interfaces/BreakMarkerConfig.js';
2
3
  import type { ForMarkerConfig } from './interfaces/ForMarkerConfig.js';
3
4
  import type { IfMarkerConfig } from './interfaces/IfMarkerConfig.js';
@@ -28,8 +29,12 @@ export declare class CodeGenerator {
28
29
  static resetGlobalState(): void;
29
30
  generateRenderMethod(template: ParsedTemplate, styles?: string): string;
30
31
  generateHtml(template: ParsedTemplate): string;
31
- generateRenderMethodFromHtml(html: string, styles?: string, markerConfigJson?: string): string;
32
- getMarkerConfigJson(): string;
32
+ generateRenderMethodFromHtml(html: string, styles?: string, markerConfigExpr?: t.Expression): string;
33
+ getMarkerConfigExpression(): t.Expression;
34
+ private buildMarkerConfigExpression;
35
+ private buildMarkerConfigObject;
36
+ private buildDepsExpression;
37
+ private buildPropertyChainExpression;
33
38
  generateBindingsSetup(): string;
34
39
  getBindingsMap(): Record<string, Record<string, unknown>[]>;
35
40
  generateExpressionAssignments(): string;
package/CodeGenerator.js CHANGED
@@ -33,8 +33,8 @@ export class CodeGenerator {
33
33
  this.markerId = 0;
34
34
  this.markerConfigs.clear();
35
35
  const html = this.generateHtml(template);
36
- const configJson = JSON.stringify(Array.from(this.markerConfigs.entries()));
37
- return this.generateRenderMethodFromHtml(html, styles, configJson);
36
+ const markerConfigExpr = this.getMarkerConfigExpression();
37
+ return this.generateRenderMethodFromHtml(html, styles, markerConfigExpr);
38
38
  }
39
39
  generateHtml(template) {
40
40
  this.rootFragment = parse5.parseFragment('');
@@ -46,7 +46,7 @@ export class CodeGenerator {
46
46
  }
47
47
  return parse5.serialize(this.rootFragment);
48
48
  }
49
- generateRenderMethodFromHtml(html, styles, markerConfigJson) {
49
+ generateRenderMethodFromHtml(html, styles, markerConfigExpr) {
50
50
  let content = html;
51
51
  if (styles) {
52
52
  const fragment = parse5.parseFragment(html);
@@ -58,14 +58,86 @@ export class CodeGenerator {
58
58
  }
59
59
  const statements = [];
60
60
  statements.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__getShadowRoot')), []), t.identifier('innerHTML')), t.stringLiteral(content))));
61
- if (markerConfigJson) {
62
- statements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__setMarkerConfigs')), [t.stringLiteral(markerConfigJson)])));
61
+ if (markerConfigExpr) {
62
+ statements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__setMarkerConfigs')), [markerConfigExpr])));
63
63
  }
64
64
  const program = t.program(statements);
65
65
  return generate(program, { compact: false }).code;
66
66
  }
67
- getMarkerConfigJson() {
68
- return JSON.stringify(Array.from(this.markerConfigs.entries()));
67
+ getMarkerConfigExpression() {
68
+ return this.buildMarkerConfigExpression();
69
+ }
70
+ buildMarkerConfigExpression() {
71
+ const entries = Array.from(this.markerConfigs.entries())
72
+ .map(([id, config]) => t.arrayExpression([
73
+ t.numericLiteral(id),
74
+ this.buildMarkerConfigObject(config)
75
+ ]));
76
+ return t.arrayExpression(entries);
77
+ }
78
+ buildMarkerConfigObject(config) {
79
+ const properties = [
80
+ t.objectProperty(t.stringLiteral('type'), t.stringLiteral(config.type))
81
+ ];
82
+ 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
+ }
93
+ }
94
+ 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)));
99
+ }
100
+ if (branch.deps) {
101
+ branchProps.push(t.objectProperty(t.stringLiteral('deps'), this.buildDepsExpression(branch.deps)));
102
+ }
103
+ return t.objectExpression(branchProps);
104
+ }))));
105
+ }
106
+ 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
+ }
114
+ }
115
+ 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) {
137
+ if (Array.isArray(dep)) {
138
+ return t.arrayExpression(dep.map(part => t.stringLiteral(part)));
139
+ }
140
+ return t.stringLiteral(dep);
69
141
  }
70
142
  generateBindingsSetup() {
71
143
  const statements = [
@@ -107,11 +179,9 @@ export class CodeGenerator {
107
179
  const normalizedHandler = CodeGenerator.normalizeCompiledExpr(h);
108
180
  return CodeGenerator.buildHandlerArrowFunction(['t', 'l', '__ev'], normalizedHandler);
109
181
  });
110
- const statements = [
111
- t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('FluffBase'), t.identifier('__e')), t.arrayExpression(exprElements))),
112
- t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('FluffBase'), t.identifier('__h')), t.arrayExpression(handlerElements)))
113
- ];
114
- const program = t.program(statements);
182
+ 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)]));
184
+ const program = t.program([fluffBaseImport, setExprTableCall]);
115
185
  return generate(program, { compact: false }).code;
116
186
  }
117
187
  static buildExpressionArrowFunction(params, bodyExpr) {
@@ -8,18 +8,18 @@ export declare class ComponentCompiler {
8
8
  private getReactivePropsForFile;
9
9
  protected createTemplateParser(_filePath: string): TemplateParser;
10
10
  private runBabelTransform;
11
- discoverComponents(dir: string): Promise<void>;
11
+ discoverComponents(dir: string): Promise<string[]>;
12
12
  compileComponentForBundle(filePath: string, minify?: boolean, sourcemap?: boolean, skipDefine?: boolean, production?: boolean): Promise<CompileResult>;
13
13
  stripTypeScriptWithSourceMap(code: string, filePath: string, sourcemap?: boolean): Promise<CompileResult>;
14
14
  transformImportsForBundle(code: string, filePath: string): Promise<string>;
15
15
  transformReactiveProperties(code: string, filePath?: string, production?: boolean): Promise<string>;
16
+ transformPipeDecorators(code: string, filePath?: string): Promise<string>;
16
17
  stripTypeScript(code: string, filePath?: string): Promise<string>;
17
18
  extractComponentMetadata(code: string, filePath: string): Promise<ComponentMetadata | null>;
18
19
  transformClass(code: string, filePath: string, options: ClassTransformOptions): Promise<string>;
19
20
  transformLibraryImports(code: string, filePath: string): Promise<string>;
20
21
  private createComponentSourceMap;
21
22
  private addFluffImport;
22
- private appendCode;
23
23
  private addBindingsMap;
24
24
  private addCustomElementsDefine;
25
25
  }
@@ -62,20 +62,24 @@ export class ComponentCompiler {
62
62
  }
63
63
  }
64
64
  async discoverComponents(dir) {
65
+ const componentPaths = [];
65
66
  const entries = fs.readdirSync(dir, { withFileTypes: true });
66
67
  for (const entry of entries) {
67
68
  const fullPath = path.join(dir, entry.name);
68
69
  if (entry.isDirectory()) {
69
- await this.discoverComponents(fullPath);
70
+ const subPaths = await this.discoverComponents(fullPath);
71
+ componentPaths.push(...subPaths);
70
72
  }
71
- else if (entry.name.endsWith('.component.ts')) {
73
+ else if (entry.name.endsWith('.ts')) {
72
74
  const content = fs.readFileSync(fullPath, 'utf-8');
73
75
  const metadata = await this.extractComponentMetadata(content, fullPath);
74
76
  if (metadata?.selector) {
75
77
  this.componentSelectors.add(metadata.selector);
78
+ componentPaths.push(fullPath);
76
79
  }
77
80
  }
78
81
  }
82
+ return componentPaths;
79
83
  }
80
84
  async compileComponentForBundle(filePath, minify, sourcemap, skipDefine, production) {
81
85
  let source = fs.readFileSync(filePath, 'utf-8');
@@ -134,20 +138,15 @@ export class ComponentCompiler {
134
138
  removeEmptyAttributes: true
135
139
  });
136
140
  }
137
- const markerConfigJson = gen.getMarkerConfigJson();
138
- const renderMethod = gen.generateRenderMethodFromHtml(generatedHtml, styles, markerConfigJson);
139
- const bindingsSetup = gen.generateBindingsSetup();
141
+ const markerConfigExpr = gen.getMarkerConfigExpression();
142
+ const renderMethod = gen.generateRenderMethodFromHtml(generatedHtml, styles, markerConfigExpr);
140
143
  let result = await this.transformImportsForBundle(source, filePath);
141
144
  result = await this.transformClass(result, filePath, {
142
145
  className, originalSuperClass: 'HTMLElement', newSuperClass: 'FluffElement', injectMethods: [
143
- { name: '__render', body: renderMethod }, { name: '__setupBindings', body: bindingsSetup }
146
+ { name: '__render', body: renderMethod }
144
147
  ]
145
148
  });
146
149
  result = this.addFluffImport(result);
147
- const exprAssignments = gen.generateExpressionAssignments();
148
- if (exprAssignments) {
149
- result = this.appendCode(result, exprAssignments);
150
- }
151
150
  const bindingsMap = gen.getBindingsMap();
152
151
  if (Object.keys(bindingsMap).length > 0) {
153
152
  result = this.addBindingsMap(result, className, bindingsMap);
@@ -207,6 +206,14 @@ export class ComponentCompiler {
207
206
  errorContext: 'Babel transform error'
208
207
  });
209
208
  }
209
+ async transformPipeDecorators(code, filePath = 'file.ts') {
210
+ return this.runBabelTransform(code, filePath, {
211
+ useTypeScriptPreset: true,
212
+ useDecoratorSyntax: true,
213
+ plugins: [reactivePlugin],
214
+ errorContext: 'Pipe decorator transform error'
215
+ });
216
+ }
210
217
  async stripTypeScript(code, filePath = 'file.ts') {
211
218
  try {
212
219
  const result = await esbuild.transform(code, {
@@ -284,19 +291,12 @@ export class ComponentCompiler {
284
291
  const ast = parse(code, { sourceType: 'module' });
285
292
  const importSpecifiers = [
286
293
  t.importSpecifier(t.identifier('FluffBase'), t.identifier('FluffBase')),
287
- t.importSpecifier(t.identifier('FluffElement'), t.identifier('FluffElement')),
288
- t.importSpecifier(t.identifier('MarkerManager'), t.identifier('MarkerManager'))
294
+ t.importSpecifier(t.identifier('FluffElement'), t.identifier('FluffElement'))
289
295
  ];
290
296
  const importDecl = t.importDeclaration(importSpecifiers, t.stringLiteral('@fluffjs/fluff'));
291
297
  ast.program.body.unshift(importDecl);
292
298
  return generate(ast, { compact: false }).code;
293
299
  }
294
- appendCode(code, additionalCode) {
295
- const ast = parse(code, { sourceType: 'module' });
296
- const additionalAst = parse(additionalCode, { sourceType: 'module' });
297
- ast.program.body.push(...additionalAst.program.body);
298
- return generate(ast, { compact: false }).code;
299
- }
300
300
  addBindingsMap(code, className, bindingsMap) {
301
301
  const ast = parse(code, { sourceType: 'module' });
302
302
  const jsonStr = JSON.stringify(bindingsMap);
@@ -8,6 +8,7 @@ export default function reactivePlugin() {
8
8
  Program: {
9
9
  enter(path, state) {
10
10
  state.needsPropertyImport = false;
11
+ state.needsPipeRegistryImport = false;
11
12
  state.reactiveProperties = new Set();
12
13
  state.watchMethods = [];
13
14
  }, exit(path, state) {
@@ -27,6 +28,18 @@ export default function reactivePlugin() {
27
28
  path.node.body.unshift(importDecl);
28
29
  }
29
30
  }
31
+ if (state.needsPipeRegistryImport) {
32
+ const hasPipeRegistryImport = path.node.body.some(node => {
33
+ if (t.isImportDeclaration(node)) {
34
+ return node.specifiers.some(spec => t.isImportSpecifier(spec) && t.isIdentifier(spec.imported) && spec.imported.name === 'pipeRegistry');
35
+ }
36
+ return false;
37
+ });
38
+ if (!hasPipeRegistryImport) {
39
+ const importDecl = t.importDeclaration([t.importSpecifier(t.identifier('pipeRegistry'), t.identifier('pipeRegistry'))], t.stringLiteral('@fluffjs/fluff'));
40
+ path.node.body.unshift(importDecl);
41
+ }
42
+ }
30
43
  }
31
44
  },
32
45
  ClassBody(path, state) {
@@ -69,6 +82,7 @@ export default function reactivePlugin() {
69
82
  const privateFields = [];
70
83
  const getterHostBindingUpdates = [];
71
84
  const propertyHostBindingInits = [];
85
+ const classHostBindingDefs = [];
72
86
  const pipeMethods = [];
73
87
  const hostListeners = [];
74
88
  const linkedPropertyMethods = [];
@@ -200,18 +214,26 @@ export default function reactivePlugin() {
200
214
  const privateName = `__hostBinding_${propName}`;
201
215
  const initialValue = propNode.value ?? t.identifier('undefined');
202
216
  const privateField = t.classProperty(t.identifier(privateName), initialValue);
203
- const getter = t.classMethod('get', t.identifier(propName), [], t.blockStatement([
204
- t.returnStatement(t.memberExpression(t.thisExpression(), t.identifier(privateName)))
205
- ]));
206
- const updateStatement = buildHostBindingUpdateStatement(hostProperty);
207
- const setter = t.classMethod('set', t.identifier(propName), [t.identifier('__v')], t.blockStatement([
208
- t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), t.identifier(privateName)), t.identifier('__v'))),
209
- updateStatement
210
- ]));
211
- newMembers.push(getter, setter);
212
- propsToRemove.push(memberPath);
213
- propertyHostBindingInits.push({ propName, privateName });
214
- path.unshiftContainer('body', privateField);
217
+ if (hostProperty.startsWith('class.')) {
218
+ const className = hostProperty.slice(6);
219
+ propsToRemove.push(memberPath);
220
+ classHostBindingDefs.push({ propName, className, privateName });
221
+ path.unshiftContainer('body', privateField);
222
+ }
223
+ else {
224
+ const getter = t.classMethod('get', t.identifier(propName), [], t.blockStatement([
225
+ t.returnStatement(t.memberExpression(t.thisExpression(), t.identifier(privateName)))
226
+ ]));
227
+ const updateStatement = buildHostBindingUpdateStatement(hostProperty);
228
+ const setter = t.classMethod('set', t.identifier(propName), [t.identifier('__v')], t.blockStatement([
229
+ t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.thisExpression(), t.identifier(privateName)), t.identifier('__v'))),
230
+ updateStatement
231
+ ]));
232
+ newMembers.push(getter, setter);
233
+ propsToRemove.push(memberPath);
234
+ propertyHostBindingInits.push({ propName, privateName });
235
+ path.unshiftContainer('body', privateField);
236
+ }
215
237
  }
216
238
  }
217
239
  continue;
@@ -234,6 +256,7 @@ export default function reactivePlugin() {
234
256
  continue;
235
257
  }
236
258
  const reactiveDecoratorIndices = [];
259
+ const decoratorsSnapshot = [...decorators];
237
260
  for (const [idx, dec] of decorators.entries()) {
238
261
  const name = getDecoratorName(dec);
239
262
  if (name === 'Reactive' || name === 'Input') {
@@ -250,31 +273,67 @@ export default function reactivePlugin() {
250
273
  const propName = propNode.key.name;
251
274
  const privateName = `__${propName}`;
252
275
  const initialValue = propNode.value ?? t.identifier('undefined');
276
+ let directionExpr = undefined;
277
+ let commitTriggerName = undefined;
253
278
  state.needsPropertyImport = true;
254
279
  state.reactiveProperties?.add(propName);
255
- const isProduction = state.opts?.production ?? false;
256
- const propertyArgs = isProduction
257
- ? [initialValue]
258
- : [
259
- t.objectExpression([
260
- t.objectProperty(t.identifier('initialValue'), initialValue),
261
- t.objectProperty(t.identifier('propertyName'), t.stringLiteral(propName))
262
- ])
263
- ];
264
- const privateField = t.classProperty(t.identifier(privateName), t.newExpression(t.identifier('Property'), propertyArgs));
265
- const getter = t.classMethod('get', t.identifier(propName), [], t.blockStatement([
266
- t.returnStatement(t.callExpression(t.memberExpression(t.memberExpression(t.thisExpression(), t.identifier(privateName)), t.identifier('getValue')), []))
267
- ]));
280
+ for (const dec of decoratorsSnapshot) {
281
+ const name = getDecoratorName(dec);
282
+ if (name !== 'Reactive' && name !== 'Input')
283
+ continue;
284
+ if (!t.isCallExpression(dec.expression))
285
+ continue;
286
+ const args = dec.expression.arguments;
287
+ if (args.length === 0)
288
+ continue;
289
+ const [optionsArg] = args;
290
+ if (!t.isObjectExpression(optionsArg))
291
+ continue;
292
+ for (const prop of optionsArg.properties) {
293
+ if (!t.isObjectProperty(prop))
294
+ continue;
295
+ if (prop.computed || !t.isIdentifier(prop.key))
296
+ continue;
297
+ if (prop.key.name === 'direction' && t.isExpression(prop.value)) {
298
+ directionExpr = prop.value;
299
+ }
300
+ if (prop.key.name === 'commitTrigger' && t.isStringLiteral(prop.value)) {
301
+ commitTriggerName = prop.value.value;
302
+ }
303
+ }
304
+ }
268
305
  const linkedMethod = linkedPropertyMethods.find(lp => lp.propertyName === propName);
269
- const setterStatements = [
270
- t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.thisExpression(), t.identifier(privateName)), t.identifier('setValue')), [t.identifier('__v')]))
271
- ];
272
- if (linkedMethod) {
273
- setterStatements.push(t.ifStatement(t.binaryExpression('instanceof', t.identifier('__v'), t.identifier('Property')), t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier(linkedMethod.methodName)), [t.identifier('__v')]))));
306
+ const isProduction = state.opts?.production ?? false;
307
+ const hasDirection = directionExpr !== undefined;
308
+ const hasCommitTrigger = commitTriggerName !== undefined;
309
+ const hasLinkedMethod = linkedMethod !== undefined;
310
+ const useOptionsObject = !isProduction || hasDirection || hasCommitTrigger || hasLinkedMethod;
311
+ const propertyOptions = [];
312
+ if (useOptionsObject) {
313
+ propertyOptions.push(t.objectProperty(t.identifier('initialValue'), initialValue));
314
+ if (!isProduction) {
315
+ propertyOptions.push(t.objectProperty(t.identifier('propertyName'), t.stringLiteral(propName)));
316
+ }
317
+ if (directionExpr) {
318
+ propertyOptions.push(t.objectProperty(t.identifier('direction'), directionExpr));
319
+ }
320
+ if (commitTriggerName) {
321
+ propertyOptions.push(t.objectProperty(t.identifier('commitTrigger'), t.memberExpression(t.thisExpression(), t.identifier(`__${commitTriggerName}`))));
322
+ }
323
+ if (linkedMethod) {
324
+ propertyOptions.push(t.objectProperty(t.identifier('linkHandler'), t.arrowFunctionExpression([
325
+ t.identifier('__p')
326
+ ], t.callExpression(t.memberExpression(t.thisExpression(), t.identifier(linkedMethod.methodName)), [
327
+ t.identifier('__p')
328
+ ]))));
329
+ }
274
330
  }
275
- const setter = t.classMethod('set', t.identifier(propName), [t.identifier('__v')], t.blockStatement(setterStatements));
331
+ const propertyArgs = useOptionsObject
332
+ ? [t.objectExpression(propertyOptions)]
333
+ : [initialValue];
334
+ const createPropCall = t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__createProp')), [t.stringLiteral(propName), ...propertyArgs]);
335
+ const privateField = t.classProperty(t.identifier(privateName), createPropCall);
276
336
  propsToRemove.push(memberPath);
277
- newMembers.push(getter, setter);
278
337
  privateFields.push(privateField);
279
338
  }
280
339
  for (const p of propsToRemove) {
@@ -321,6 +380,9 @@ export default function reactivePlugin() {
321
380
  }
322
381
  const reactiveProps = state.reactiveProperties ?? new Set();
323
382
  const constructorStatements = [];
383
+ for (const { propName, className, privateName } of classHostBindingDefs) {
384
+ constructorStatements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__defineClassHostBinding')), [t.stringLiteral(propName), t.stringLiteral(className), t.stringLiteral(privateName)])));
385
+ }
324
386
  for (const updateMethodName of getterHostBindingUpdates) {
325
387
  constructorStatements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier(updateMethodName)), [])));
326
388
  }
@@ -394,6 +456,29 @@ export default function reactivePlugin() {
394
456
  ])));
395
457
  }
396
458
  }
459
+ },
460
+ ClassDeclaration(path, state) {
461
+ const decorators = path.node.decorators ?? [];
462
+ const pipeDecoratorIndex = findDecoratorIndex(decorators, 'Pipe');
463
+ if (pipeDecoratorIndex >= 0) {
464
+ const pipeDecorator = decorators[pipeDecoratorIndex];
465
+ if (t.isCallExpression(pipeDecorator.expression)) {
466
+ const args = pipeDecorator.expression.arguments;
467
+ if (args.length > 0 && t.isStringLiteral(args[0]) && path.node.id) {
468
+ const pipeName = args[0].value;
469
+ const className = path.node.id;
470
+ state.needsPipeRegistryImport = true;
471
+ decorators.splice(pipeDecoratorIndex, 1);
472
+ if (decorators.length === 0) {
473
+ path.node.decorators = null;
474
+ }
475
+ path.insertAfter([
476
+ t.expressionStatement(t.assignmentExpression('=', t.memberExpression(className, t.identifier('__pipeName')), t.stringLiteral(pipeName))),
477
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('pipeRegistry'), t.identifier('set')), [t.stringLiteral(pipeName), className]))
478
+ ]);
479
+ }
480
+ }
481
+ }
397
482
  }
398
483
  }
399
484
  };
@@ -1,8 +1,12 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { parse } from '@babel/parser';
5
+ import * as t from '@babel/types';
6
+ import { generate } from './BabelHelpers.js';
4
7
  import { CodeGenerator } from './CodeGenerator.js';
5
8
  import { ComponentCompiler } from './ComponentCompiler.js';
9
+ const VIRTUAL_EXPR_TABLE_ID = '@fluff/expr-table';
6
10
  function findFluffSourcePath() {
7
11
  const thisFile = fileURLToPath(import.meta.url);
8
12
  const distDir = path.dirname(thisFile);
@@ -14,28 +18,140 @@ function findFluffSourcePath() {
14
18
  }
15
19
  return null;
16
20
  }
21
+ function getEntryPointPath(build) {
22
+ const { entryPoints, stdin } = build.initialOptions;
23
+ if (stdin?.resolveDir) {
24
+ return null;
25
+ }
26
+ if (Array.isArray(entryPoints) && entryPoints.length > 0) {
27
+ const [first] = entryPoints;
28
+ return typeof first === 'string' ? first : first.in;
29
+ }
30
+ return null;
31
+ }
32
+ function detectDecorators(source) {
33
+ const result = { hasComponent: false, hasPipe: false };
34
+ try {
35
+ const ast = parse(source, {
36
+ sourceType: 'module',
37
+ plugins: ['typescript', 'decorators']
38
+ });
39
+ for (const node of ast.program.body) {
40
+ if (t.isClassDeclaration(node) && node.decorators) {
41
+ for (const decorator of node.decorators) {
42
+ if (t.isCallExpression(decorator.expression) && t.isIdentifier(decorator.expression.callee)) {
43
+ const { name } = decorator.expression.callee;
44
+ if (name === 'Component')
45
+ result.hasComponent = true;
46
+ if (name === 'Pipe')
47
+ result.hasPipe = true;
48
+ }
49
+ }
50
+ }
51
+ if (t.isExportNamedDeclaration(node) && t.isClassDeclaration(node.declaration) && node.declaration.decorators) {
52
+ for (const decorator of node.declaration.decorators) {
53
+ if (t.isCallExpression(decorator.expression) && t.isIdentifier(decorator.expression.callee)) {
54
+ const { name } = decorator.expression.callee;
55
+ if (name === 'Component')
56
+ result.hasComponent = true;
57
+ if (name === 'Pipe')
58
+ result.hasPipe = true;
59
+ }
60
+ }
61
+ }
62
+ if (t.isExportDefaultDeclaration(node) && t.isClassDeclaration(node.declaration) && node.declaration.decorators) {
63
+ for (const decorator of node.declaration.decorators) {
64
+ if (t.isCallExpression(decorator.expression) && t.isIdentifier(decorator.expression.callee)) {
65
+ const { name } = decorator.expression.callee;
66
+ if (name === 'Component')
67
+ result.hasComponent = true;
68
+ if (name === 'Pipe')
69
+ result.hasPipe = true;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ catch {
76
+ }
77
+ return result;
78
+ }
17
79
  export function fluffPlugin(options) {
18
80
  const compiler = new ComponentCompiler();
19
- let componentsDiscovered = false;
20
81
  const fluffSrcPath = findFluffSourcePath();
82
+ const compiledCache = new Map();
83
+ let entryPointPath = null;
21
84
  // noinspection JSUnusedGlobalSymbols
22
85
  return {
23
86
  name: 'fluff',
24
87
  setup(build) {
88
+ entryPointPath = getEntryPointPath(build);
25
89
  build.onStart(async () => {
26
90
  CodeGenerator.resetGlobalState();
27
- if (!componentsDiscovered) {
28
- await compiler.discoverComponents(options.srcDir);
29
- componentsDiscovered = true;
91
+ compiledCache.clear();
92
+ const componentPaths = await compiler.discoverComponents(options.srcDir);
93
+ for (const componentPath of componentPaths) {
94
+ const result = await compiler.compileComponentForBundle(componentPath, options.minify, options.sourcemap, options.skipDefine, options.production);
95
+ compiledCache.set(componentPath, {
96
+ code: result.code,
97
+ watchFiles: result.watchFiles
98
+ });
99
+ }
100
+ });
101
+ build.onLoad({ filter: /\.ts$/ }, async (args) => {
102
+ const source = fs.readFileSync(args.path, 'utf-8');
103
+ const decorators = detectDecorators(source);
104
+ if (decorators.hasComponent) {
105
+ const cached = compiledCache.get(args.path);
106
+ if (cached) {
107
+ return {
108
+ contents: cached.code,
109
+ loader: 'js',
110
+ resolveDir: path.dirname(args.path),
111
+ watchFiles: cached.watchFiles
112
+ };
113
+ }
114
+ const result = await compiler.compileComponentForBundle(args.path, options.minify, options.sourcemap, options.skipDefine, options.production);
115
+ return {
116
+ contents: result.code,
117
+ loader: 'js',
118
+ resolveDir: path.dirname(args.path),
119
+ watchFiles: result.watchFiles
120
+ };
121
+ }
122
+ if (decorators.hasPipe) {
123
+ const transformed = await compiler.transformPipeDecorators(source, args.path);
124
+ return {
125
+ contents: transformed,
126
+ loader: 'ts',
127
+ resolveDir: path.dirname(args.path)
128
+ };
30
129
  }
130
+ if (!entryPointPath || args.path !== entryPointPath) {
131
+ return null;
132
+ }
133
+ const ast = parse(source, {
134
+ sourceType: 'module',
135
+ plugins: ['typescript', 'decorators']
136
+ });
137
+ const exprTableImport = t.importDeclaration([], t.stringLiteral(VIRTUAL_EXPR_TABLE_ID));
138
+ ast.program.body.push(exprTableImport);
139
+ const output = generate(ast, { compact: false });
140
+ return {
141
+ contents: output.code,
142
+ loader: 'ts',
143
+ resolveDir: path.dirname(args.path)
144
+ };
145
+ });
146
+ build.onResolve({ filter: new RegExp(`^${VIRTUAL_EXPR_TABLE_ID.replace('/', '\\/')}$`) }, () => {
147
+ return { path: VIRTUAL_EXPR_TABLE_ID, namespace: 'fluff-virtual' };
31
148
  });
32
- build.onLoad({ filter: /\.component\.ts$/ }, async (args) => {
33
- const result = await compiler.compileComponentForBundle(args.path, options.minify, options.sourcemap, options.skipDefine, options.production);
149
+ build.onLoad({ filter: /.*/, namespace: 'fluff-virtual' }, () => {
150
+ const exprTable = CodeGenerator.generateGlobalExprTable();
34
151
  return {
35
- contents: result.code,
152
+ contents: exprTable || '',
36
153
  loader: 'js',
37
- resolveDir: path.dirname(args.path),
38
- watchFiles: result.watchFiles
154
+ resolveDir: options.srcDir
39
155
  };
40
156
  });
41
157
  if (fluffSrcPath) {
@@ -3,6 +3,7 @@ import type { BabelPluginReactiveWatchInfo } from './BabelPluginReactiveWatchInf
3
3
  export interface BabelPluginReactiveState {
4
4
  filename?: string;
5
5
  needsPropertyImport?: boolean;
6
+ needsPipeRegistryImport?: boolean;
6
7
  reactiveProperties?: Set<string>;
7
8
  watchMethods?: BabelPluginReactiveWatchInfo[];
8
9
  watchCalls?: BabelPluginReactiveWatchCallInfo[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluffjs/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -0,0 +1,22 @@
1
+ interface MarkerConfigLiteralRecord {
2
+ [key: string]: MarkerConfigLiteral;
3
+ }
4
+ interface MarkerConfigLiteralArray extends Array<MarkerConfigLiteral> {
5
+ readonly __markerConfigArrayBrand?: true;
6
+ }
7
+ type MarkerConfigLiteral = string | number | boolean | null | MarkerConfigLiteralArray | MarkerConfigLiteralRecord;
8
+ type MarkerConfigEntriesLiteral = [number, MarkerConfigLiteral][];
9
+ export declare class MarkerConfigAstReader {
10
+ static readMarkerConfigEntries(code: string): MarkerConfigEntriesLiteral;
11
+ static collectDeps(entries: MarkerConfigEntriesLiteral): string[];
12
+ private static collectDepsFromRecord;
13
+ private static collectDepsFromIf;
14
+ private static collectStringsFromDep;
15
+ private static isMarkerConfigCall;
16
+ private static evaluateLiteral;
17
+ private static readObjectKey;
18
+ private static isEntriesArray;
19
+ private static isRecord;
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=MarkerConfigAstReader.d.ts.map
@@ -0,0 +1,162 @@
1
+ import { parse } from '@babel/parser';
2
+ import * as t from '@babel/types';
3
+ import { traverse } from '../BabelHelpers.js';
4
+ export class MarkerConfigAstReader {
5
+ static readMarkerConfigEntries(code) {
6
+ const ast = parse(code, {
7
+ sourceType: 'module',
8
+ plugins: ['typescript', 'decorators']
9
+ });
10
+ let markerConfigArg = null;
11
+ traverse(ast, {
12
+ CallExpression(path) {
13
+ if (MarkerConfigAstReader.isMarkerConfigCall(path.node)) {
14
+ const [firstArg] = path.node.arguments;
15
+ if (firstArg && t.isExpression(firstArg)) {
16
+ markerConfigArg = firstArg;
17
+ path.stop();
18
+ }
19
+ }
20
+ }
21
+ });
22
+ if (!markerConfigArg) {
23
+ throw new Error('Could not find __setMarkerConfigs call');
24
+ }
25
+ const literal = MarkerConfigAstReader.evaluateLiteral(markerConfigArg);
26
+ if (!MarkerConfigAstReader.isEntriesArray(literal)) {
27
+ throw new Error('Expected marker config entries array');
28
+ }
29
+ return literal;
30
+ }
31
+ static collectDeps(entries) {
32
+ const deps = [];
33
+ for (const [, config] of entries) {
34
+ if (!MarkerConfigAstReader.isRecord(config)) {
35
+ continue;
36
+ }
37
+ const configRecord = config;
38
+ const typeValue = configRecord.type;
39
+ if (typeof typeValue !== 'string') {
40
+ continue;
41
+ }
42
+ switch (typeValue) {
43
+ case 'text':
44
+ MarkerConfigAstReader.collectDepsFromRecord(configRecord, deps);
45
+ break;
46
+ case 'if':
47
+ MarkerConfigAstReader.collectDepsFromIf(configRecord, deps);
48
+ break;
49
+ case 'for':
50
+ MarkerConfigAstReader.collectDepsFromRecord(configRecord, deps);
51
+ break;
52
+ case 'switch':
53
+ MarkerConfigAstReader.collectDepsFromRecord(configRecord, deps);
54
+ break;
55
+ }
56
+ }
57
+ return deps;
58
+ }
59
+ static collectDepsFromRecord(configRecord, deps) {
60
+ const configDeps = configRecord.deps;
61
+ if (Array.isArray(configDeps)) {
62
+ for (const dep of configDeps) {
63
+ MarkerConfigAstReader.collectStringsFromDep(dep, deps);
64
+ }
65
+ }
66
+ }
67
+ static collectDepsFromIf(configRecord, deps) {
68
+ const { branches } = configRecord;
69
+ if (!Array.isArray(branches)) {
70
+ return;
71
+ }
72
+ for (const branch of branches) {
73
+ if (!MarkerConfigAstReader.isRecord(branch)) {
74
+ continue;
75
+ }
76
+ MarkerConfigAstReader.collectDepsFromRecord(branch, deps);
77
+ }
78
+ }
79
+ static collectStringsFromDep(dep, deps) {
80
+ if (typeof dep === 'string') {
81
+ deps.push(dep);
82
+ return;
83
+ }
84
+ if (Array.isArray(dep)) {
85
+ for (const item of dep) {
86
+ MarkerConfigAstReader.collectStringsFromDep(item, deps);
87
+ }
88
+ }
89
+ }
90
+ static isMarkerConfigCall(node) {
91
+ if (!t.isMemberExpression(node.callee)) {
92
+ return false;
93
+ }
94
+ if (!t.isIdentifier(node.callee.property)) {
95
+ return false;
96
+ }
97
+ return node.callee.property.name === '__setMarkerConfigs';
98
+ }
99
+ static evaluateLiteral(node) {
100
+ if (t.isStringLiteral(node)) {
101
+ return node.value;
102
+ }
103
+ if (t.isNumericLiteral(node)) {
104
+ return node.value;
105
+ }
106
+ if (t.isBooleanLiteral(node)) {
107
+ return node.value;
108
+ }
109
+ if (t.isNullLiteral(node)) {
110
+ return null;
111
+ }
112
+ if (t.isArrayExpression(node)) {
113
+ const items = [];
114
+ for (const element of node.elements) {
115
+ if (!element || !t.isExpression(element)) {
116
+ throw new Error('Unexpected array element in marker config literal');
117
+ }
118
+ items.push(MarkerConfigAstReader.evaluateLiteral(element));
119
+ }
120
+ return items;
121
+ }
122
+ if (t.isObjectExpression(node)) {
123
+ const result = {};
124
+ for (const prop of node.properties) {
125
+ if (!t.isObjectProperty(prop)) {
126
+ throw new Error('Unexpected object property in marker config literal');
127
+ }
128
+ const key = MarkerConfigAstReader.readObjectKey(prop.key);
129
+ if (!t.isExpression(prop.value)) {
130
+ throw new Error('Unexpected object property value in marker config literal');
131
+ }
132
+ result[key] = MarkerConfigAstReader.evaluateLiteral(prop.value);
133
+ }
134
+ return result;
135
+ }
136
+ throw new Error('Unsupported marker config literal');
137
+ }
138
+ static readObjectKey(key) {
139
+ if (t.isIdentifier(key)) {
140
+ return key.name;
141
+ }
142
+ if (t.isStringLiteral(key)) {
143
+ return key.value;
144
+ }
145
+ throw new Error('Unsupported object key in marker config literal');
146
+ }
147
+ static isEntriesArray(value) {
148
+ if (!Array.isArray(value)) {
149
+ return false;
150
+ }
151
+ return value.every(entry => {
152
+ if (!Array.isArray(entry) || entry.length !== 2) {
153
+ return false;
154
+ }
155
+ const [id, config] = entry;
156
+ return typeof id === 'number' && MarkerConfigAstReader.isRecord(config);
157
+ });
158
+ }
159
+ static isRecord(value) {
160
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
161
+ }
162
+ }