@fluffjs/cli 0.4.5 → 0.5.0

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.
Files changed (47) hide show
  1. package/BabelHelpers.js +8 -5
  2. package/Cli.d.ts +9 -5
  3. package/Cli.js +218 -155
  4. package/CodeGenerator.d.ts +19 -10
  5. package/CodeGenerator.js +146 -106
  6. package/ComponentCompiler.d.ts +19 -3
  7. package/ComponentCompiler.js +175 -47
  8. package/DevServer.d.ts +6 -4
  9. package/DevServer.js +102 -23
  10. package/DomPreProcessor.js +22 -40
  11. package/IndexHtmlTransformer.js +20 -28
  12. package/PluginLoader.d.ts +22 -0
  13. package/PluginLoader.js +286 -0
  14. package/PluginManager.d.ts +39 -0
  15. package/PluginManager.js +209 -0
  16. package/TemplateParser.d.ts +5 -0
  17. package/TemplateParser.js +55 -0
  18. package/babel-plugin-directive.d.ts +12 -0
  19. package/babel-plugin-directive.js +78 -0
  20. package/babel-plugin-reactive.js +19 -14
  21. package/fluff-esbuild-plugin.js +66 -22
  22. package/interfaces/ClassTransformContext.d.ts +8 -0
  23. package/interfaces/ClassTransformContext.js +1 -0
  24. package/interfaces/CodeGenContext.d.ts +9 -0
  25. package/interfaces/CodeGenContext.js +1 -0
  26. package/interfaces/DiscoveryInfo.d.ts +15 -0
  27. package/interfaces/DiscoveryInfo.js +1 -0
  28. package/interfaces/EntryPointContext.d.ts +6 -0
  29. package/interfaces/EntryPointContext.js +1 -0
  30. package/interfaces/FluffConfigInterface.d.ts +2 -0
  31. package/interfaces/FluffPlugin.d.ts +26 -0
  32. package/interfaces/FluffPlugin.js +1 -0
  33. package/interfaces/FluffPluginOptions.d.ts +3 -0
  34. package/interfaces/FluffTarget.d.ts +1 -0
  35. package/interfaces/HtmlTransformOptions.d.ts +2 -0
  36. package/interfaces/PluginCustomTable.d.ts +6 -0
  37. package/interfaces/PluginCustomTable.js +1 -0
  38. package/interfaces/PluginHookDependency.d.ts +5 -0
  39. package/interfaces/PluginHookDependency.js +1 -0
  40. package/interfaces/PluginHookName.d.ts +2 -0
  41. package/interfaces/PluginHookName.js +1 -0
  42. package/interfaces/ScopeElementConfig.d.ts +5 -0
  43. package/interfaces/ScopeElementConfig.js +1 -0
  44. package/interfaces/index.d.ts +8 -0
  45. package/package.json +5 -3
  46. package/PeerDependencies.d.ts +0 -6
  47. package/PeerDependencies.js +0 -7
@@ -5,12 +5,13 @@ import cssnano from 'cssnano';
5
5
  import * as esbuild from 'esbuild';
6
6
  import * as fs from 'fs';
7
7
  import { minify as minifyHtml } from 'html-minifier-terser';
8
- import postcss from 'postcss';
9
8
  import * as parse5 from 'parse5';
10
9
  import * as path from 'path';
10
+ import postcss from 'postcss';
11
11
  import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
12
12
  import classTransformPlugin from './babel-plugin-class-transform.js';
13
13
  import componentPlugin, { componentMetadataMap } from './babel-plugin-component.js';
14
+ import directivePlugin, { directiveMetadataMap } from './babel-plugin-directive.js';
14
15
  import importsPlugin from './babel-plugin-imports.js';
15
16
  import reactivePlugin, { reactivePropertiesMap } from './babel-plugin-reactive.js';
16
17
  import { generate } from './BabelHelpers.js';
@@ -22,47 +23,20 @@ import { Parse5Helpers } from './Parse5Helpers.js';
22
23
  import { TemplateParser } from './TemplateParser.js';
23
24
  export class ComponentCompiler {
24
25
  componentSelectors = new Set();
25
- getReactivePropsForFile(filePath) {
26
- const direct = reactivePropertiesMap.get(filePath);
27
- if (direct) {
28
- return direct;
29
- }
30
- for (const [key, value] of reactivePropertiesMap.entries()) {
31
- if (key === filePath || key.endsWith(filePath) || filePath.endsWith(key)) {
32
- return value;
33
- }
34
- }
35
- return new Set();
26
+ directiveSelectors = new Set();
27
+ directivePaths = [];
28
+ pluginManager = null;
29
+ setPluginManager(manager) {
30
+ this.pluginManager = manager;
36
31
  }
37
- createTemplateParser(_filePath) {
38
- return new TemplateParser();
32
+ getDirectivePaths() {
33
+ return [...this.directivePaths];
39
34
  }
40
- async runBabelTransform(code, filePath, options) {
41
- try {
42
- const presets = options.useTypeScriptPreset
43
- ? [['@babel/preset-typescript', { isTSX: false, allExtensions: true }]]
44
- : [];
45
- const plugins = options.useDecoratorSyntax
46
- ? [['@babel/plugin-syntax-decorators', { version: '2023-11' }], ...options.plugins]
47
- : options.plugins;
48
- const parserOpts = options.useDecoratorSyntax
49
- ? { plugins: ['typescript', 'decorators'] }
50
- : options.useTypeScriptPreset
51
- ? { plugins: ['typescript'] }
52
- : undefined;
53
- const result = await babel.transformAsync(code, {
54
- filename: filePath,
55
- presets,
56
- plugins,
57
- parserOpts,
58
- cwd: path.dirname(new URL(import.meta.url).pathname)
59
- });
60
- return result?.code ?? code;
61
- }
62
- catch (e) {
63
- console.error(`${options.errorContext} in ${filePath}:`, ErrorHelpers.getErrorMessage(e));
64
- return code;
65
- }
35
+ getComponentMetadata(filePath) {
36
+ return componentMetadataMap.get(filePath) ?? null;
37
+ }
38
+ getDirectiveMetadata(filePath) {
39
+ return directiveMetadataMap.get(filePath) ?? null;
66
40
  }
67
41
  async discoverComponents(dir) {
68
42
  const componentPaths = [];
@@ -75,11 +49,18 @@ export class ComponentCompiler {
75
49
  }
76
50
  else if (entry.name.endsWith('.ts')) {
77
51
  const content = fs.readFileSync(fullPath, 'utf-8');
78
- const metadata = await this.extractComponentMetadata(content, fullPath);
79
- if (metadata?.selector) {
80
- this.componentSelectors.add(metadata.selector);
52
+ const componentMeta = await this.extractComponentMetadata(content, fullPath);
53
+ if (componentMeta?.selector) {
54
+ this.componentSelectors.add(componentMeta.selector);
81
55
  componentPaths.push(fullPath);
82
56
  }
57
+ else {
58
+ const directiveResult = await this.extractDirectiveMetadata(content, fullPath);
59
+ if (directiveResult?.metadata.selector) {
60
+ this.directiveSelectors.add(directiveResult.metadata.selector);
61
+ this.directivePaths.push(fullPath);
62
+ }
63
+ }
83
64
  }
84
65
  }
85
66
  return componentPaths;
@@ -118,7 +99,8 @@ export class ComponentCompiler {
118
99
  styles = inlineStyles;
119
100
  }
120
101
  if (minify && styles) {
121
- const nanoResult = await postcss([cssnano({ preset: 'default' })]).process(styles, { from: undefined });
102
+ const nanoResult = await postcss([cssnano({ preset: 'default' })])
103
+ .process(styles, { from: undefined });
122
104
  styles = nanoResult.css;
123
105
  const cssResult = await esbuild.transform(styles, {
124
106
  loader: 'css', minify: true
@@ -128,9 +110,20 @@ export class ComponentCompiler {
128
110
  const reactiveProps = this.getReactivePropsForFile(filePath);
129
111
  const getterDepMap = GetterDependencyExtractor.extractGetterDependencyMap(source, reactiveProps);
130
112
  parser.setGetterDependencyMap(getterDepMap);
113
+ if (this.pluginManager?.hasHook('registerScopeElements')) {
114
+ parser.setScopeElements(this.pluginManager.collectScopeElements());
115
+ }
116
+ if (this.pluginManager?.hasHook('beforeTemplatePreProcess')) {
117
+ const preProcessFragment = parse5.parseFragment(templateHtml);
118
+ await this.pluginManager.runBeforeTemplatePreProcess(preProcessFragment, selector);
119
+ templateHtml = parse5.serialize(preProcessFragment);
120
+ }
131
121
  const parsed = await parser.parse(templateHtml);
132
122
  parser.setGetterDependencyMap(new Map());
133
- const gen = new CodeGenerator(this.componentSelectors, selector);
123
+ if (this.pluginManager?.hasHook('afterTemplateParse')) {
124
+ await this.pluginManager.runAfterTemplateParse(parsed, selector);
125
+ }
126
+ const gen = new CodeGenerator(this.componentSelectors, selector, this.directiveSelectors);
134
127
  let generatedHtml = gen.generateHtml(parsed);
135
128
  if (minify) {
136
129
  const fragment = parse5.parseFragment(generatedHtml);
@@ -143,9 +136,27 @@ export class ComponentCompiler {
143
136
  removeEmptyAttributes: true
144
137
  });
145
138
  }
139
+ if (this.pluginManager?.hasHook('afterCodeGeneration')) {
140
+ const rootFragment = gen.getRootFragment();
141
+ if (rootFragment) {
142
+ await this.pluginManager.runAfterCodeGeneration({
143
+ componentSelector: selector,
144
+ generatedFragment: rootFragment,
145
+ markerConfigs: gen.getMarkerConfigsRef(),
146
+ bindingsMap: gen.getBindingsMapRef()
147
+ });
148
+ }
149
+ }
146
150
  const markerConfigExpr = gen.getMarkerConfigExpression();
147
151
  const renderMethod = gen.generateRenderMethodFromHtml(generatedHtml, styles, markerConfigExpr);
148
152
  let result = await this.transformImportsForBundle(source, filePath);
153
+ if (this.pluginManager?.hasHook('beforeClassTransform')) {
154
+ const classAst = parse(result, { sourceType: 'module', plugins: ['typescript', 'decorators'] });
155
+ await this.pluginManager.runBeforeClassTransform({
156
+ ast: classAst, filePath, metadata
157
+ });
158
+ result = generate(classAst, { compact: false }).code;
159
+ }
149
160
  result = await this.transformClass(result, filePath, {
150
161
  className, originalSuperClass: 'HTMLElement', newSuperClass: 'FluffElement', injectMethods: [
151
162
  { name: '__render', body: renderMethod }
@@ -193,7 +204,7 @@ export class ComponentCompiler {
193
204
  async transformImportsForBundle(code, filePath) {
194
205
  const importOptions = {
195
206
  removeImportsFrom: ['lighter'],
196
- removeDecorators: ['Component', 'Input', 'Output'],
207
+ removeDecorators: ['Component', 'Directive', 'Input', 'Output'],
197
208
  pathReplacements: {},
198
209
  addJsExtension: false
199
210
  };
@@ -248,6 +259,48 @@ export class ComponentCompiler {
248
259
  return null;
249
260
  }
250
261
  }
262
+ async extractDirectiveMetadata(code, filePath) {
263
+ try {
264
+ directiveMetadataMap.delete(filePath);
265
+ const result = await this.runBabelTransform(code, filePath, {
266
+ useTypeScriptPreset: true,
267
+ useDecoratorSyntax: true,
268
+ plugins: [directivePlugin],
269
+ errorContext: 'Failed to extract directive metadata'
270
+ });
271
+ const metadata = directiveMetadataMap.get(filePath);
272
+ if (!metadata)
273
+ return null;
274
+ return { metadata, transformedCode: result };
275
+ }
276
+ catch (e) {
277
+ console.error(`Failed to extract directive metadata from ${filePath}:`, ErrorHelpers.getErrorMessage(e));
278
+ return null;
279
+ }
280
+ }
281
+ async compileDirectiveForBundle(filePath, sourcemap, production) {
282
+ let source = fs.readFileSync(filePath, 'utf-8');
283
+ reactivePropertiesMap.delete(filePath);
284
+ if (source.includes('@Reactive') || source.includes('@Input') || source.includes('@HostListener') || source.includes('@HostBinding') || source.includes('@Watch') || source.includes('@LinkedProperty') || source.includes('@HostElement')) {
285
+ source = await this.transformReactiveProperties(source, filePath, production);
286
+ }
287
+ const extractResult = await this.extractDirectiveMetadata(source, filePath);
288
+ if (!extractResult) {
289
+ return { code: source };
290
+ }
291
+ const { metadata, transformedCode } = extractResult;
292
+ const { className, selector } = metadata;
293
+ source = transformedCode;
294
+ let result = await this.transformImportsForBundle(source, filePath);
295
+ result = await this.transformClass(result, filePath, {
296
+ className, originalSuperClass: 'HTMLElement', newSuperClass: 'FluffDirective', injectMethods: []
297
+ });
298
+ result = this.addFluffDirectiveImport(result);
299
+ result = this.addDirectiveRegistration(result, selector, className);
300
+ DecoratorValidator.validate(result, filePath);
301
+ const tsResult = await this.stripTypeScriptWithSourceMap(result, filePath, sourcemap);
302
+ return { code: tsResult.code };
303
+ }
251
304
  async transformClass(code, filePath, options) {
252
305
  return this.runBabelTransform(code, filePath, {
253
306
  useTypeScriptPreset: false,
@@ -269,6 +322,78 @@ export class ComponentCompiler {
269
322
  errorContext: 'Failed to transform library imports'
270
323
  });
271
324
  }
325
+ createTemplateParser(_filePath) {
326
+ return new TemplateParser();
327
+ }
328
+ getReactivePropsForFile(filePath) {
329
+ const direct = reactivePropertiesMap.get(filePath);
330
+ if (direct) {
331
+ return direct;
332
+ }
333
+ for (const [key, value] of reactivePropertiesMap.entries()) {
334
+ if (key === filePath || key.endsWith(filePath) || filePath.endsWith(key)) {
335
+ return value;
336
+ }
337
+ }
338
+ return new Set();
339
+ }
340
+ async runBabelTransform(code, filePath, options) {
341
+ try {
342
+ const presets = options.useTypeScriptPreset ? [
343
+ [
344
+ '@babel/preset-typescript',
345
+ { isTSX: false, allExtensions: true }
346
+ ]
347
+ ] : [];
348
+ const plugins = options.useDecoratorSyntax ? [
349
+ [
350
+ '@babel/plugin-syntax-decorators',
351
+ { version: '2023-11' }
352
+ ], ...options.plugins
353
+ ] : options.plugins;
354
+ const parserOpts = options.useDecoratorSyntax ? {
355
+ plugins: [
356
+ 'typescript',
357
+ 'decorators'
358
+ ]
359
+ } : options.useTypeScriptPreset ? { plugins: ['typescript'] } : undefined;
360
+ const result = await babel.transformAsync(code, {
361
+ filename: filePath, presets, plugins, parserOpts, cwd: path.dirname(new URL(import.meta.url).pathname)
362
+ });
363
+ return result?.code ?? code;
364
+ }
365
+ catch (e) {
366
+ console.error(`${options.errorContext} in ${filePath}:`, ErrorHelpers.getErrorMessage(e));
367
+ return code;
368
+ }
369
+ }
370
+ addDirectiveRegistration(code, selector, className) {
371
+ const ast = parse(code, { sourceType: 'module' });
372
+ const tagName = 'x-fluff-dir-' + className.toLowerCase()
373
+ .replace(/directive$/, '');
374
+ const defineCall = t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('customElements'), t.identifier('define')), [
375
+ t.stringLiteral(tagName),
376
+ t.identifier(className)
377
+ ]));
378
+ ast.program.body.push(defineCall);
379
+ const registerCall = t.expressionStatement(t.callExpression(t.identifier('__registerDirective'), [
380
+ t.stringLiteral(selector),
381
+ t.identifier(className)
382
+ ]));
383
+ ast.program.body.push(registerCall);
384
+ return generate(ast, { compact: false }).code;
385
+ }
386
+ addFluffDirectiveImport(code) {
387
+ const ast = parse(code, { sourceType: 'module', plugins: ['typescript', 'decorators'] });
388
+ const importSpecifiers = [
389
+ t.importSpecifier(t.identifier('FluffBase'), t.identifier('FluffBase')),
390
+ t.importSpecifier(t.identifier('FluffDirective'), t.identifier('FluffDirective')),
391
+ t.importSpecifier(t.identifier('__registerDirective'), t.identifier('__registerDirective'))
392
+ ];
393
+ const importDecl = t.importDeclaration(importSpecifiers, t.stringLiteral('@fluffjs/fluff'));
394
+ ast.program.body.unshift(importDecl);
395
+ return generate(ast, { compact: false }).code;
396
+ }
272
397
  async createComponentSourceMap(code, esbuildMap, componentPath, templatePath, stylePath) {
273
398
  const consumer = await new SourceMapConsumer(JSON.parse(esbuildMap));
274
399
  const generator = new SourceMapGenerator({
@@ -317,7 +442,10 @@ export class ComponentCompiler {
317
442
  }
318
443
  addCustomElementsDefine(code, selector, className) {
319
444
  const ast = parse(code, { sourceType: 'module' });
320
- const defineCall = t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('customElements'), t.identifier('define')), [t.stringLiteral(selector), t.identifier(className)]));
445
+ const defineCall = t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('customElements'), t.identifier('define')), [
446
+ t.stringLiteral(selector),
447
+ t.identifier(className)
448
+ ]));
321
449
  ast.program.body.push(defineCall);
322
450
  return generate(ast, { compact: false }).code;
323
451
  }
package/DevServer.d.ts CHANGED
@@ -6,22 +6,24 @@ export type ProxyConfig = Record<string, ProxyConfigEntry>;
6
6
  export interface DevServerOptions {
7
7
  port: number;
8
8
  host: string;
9
- esbuildPort: number;
10
- esbuildHost: string;
9
+ outDir: string;
11
10
  proxyConfig?: ProxyConfig;
12
11
  }
13
12
  export declare class DevServer {
14
13
  private readonly options;
15
- private readonly proxy;
14
+ private proxy;
16
15
  private server;
16
+ private wss;
17
+ private readonly wsClients;
17
18
  constructor(options: DevServerOptions);
18
19
  static loadProxyConfig(configPath: string): ProxyConfig | undefined;
19
20
  private static isProxyEntry;
20
21
  start(): Promise<number>;
22
+ notifyReload(): void;
21
23
  stop(): void;
22
24
  private handleRequest;
23
25
  private findProxyEntry;
24
26
  private proxyRequest;
25
- private forwardToEsbuild;
27
+ private serveStatic;
26
28
  }
27
29
  //# sourceMappingURL=DevServer.d.ts.map
package/DevServer.js CHANGED
@@ -1,20 +1,49 @@
1
1
  import httpProxy from 'http-proxy';
2
2
  import * as fs from 'node:fs';
3
3
  import * as http from 'node:http';
4
+ import * as path from 'node:path';
5
+ import { WebSocketServer } from 'ws';
6
+ const MIME_TYPES = {
7
+ '.html': 'text/html; charset=utf-8',
8
+ '.js': 'application/javascript; charset=utf-8',
9
+ '.mjs': 'application/javascript; charset=utf-8',
10
+ '.css': 'text/css; charset=utf-8',
11
+ '.json': 'application/json; charset=utf-8',
12
+ '.map': 'application/json; charset=utf-8',
13
+ '.svg': 'image/svg+xml',
14
+ '.png': 'image/png',
15
+ '.jpg': 'image/jpeg',
16
+ '.jpeg': 'image/jpeg',
17
+ '.gif': 'image/gif',
18
+ '.webp': 'image/webp',
19
+ '.ico': 'image/x-icon',
20
+ '.woff': 'font/woff',
21
+ '.woff2': 'font/woff2',
22
+ '.ttf': 'font/ttf',
23
+ '.eot': 'application/vnd.ms-fontobject',
24
+ '.otf': 'font/otf',
25
+ '.wasm': 'application/wasm',
26
+ '.txt': 'text/plain; charset=utf-8',
27
+ '.xml': 'application/xml; charset=utf-8'
28
+ };
4
29
  export class DevServer {
5
30
  options;
6
- proxy;
31
+ proxy = null;
7
32
  server = null;
33
+ wss = null;
34
+ wsClients = new Set();
8
35
  constructor(options) {
9
36
  this.options = options;
10
- this.proxy = httpProxy.createProxyServer({});
11
- this.proxy.on('error', (err, req, res) => {
12
- console.error('Proxy error:', err.message);
13
- if (res instanceof http.ServerResponse && !res.headersSent) {
14
- res.writeHead(502, { 'Content-Type': 'text/plain' });
15
- res.end('Proxy error: ' + err.message);
16
- }
17
- });
37
+ if (options.proxyConfig) {
38
+ this.proxy = httpProxy.createProxyServer({});
39
+ this.proxy.on('error', (err, req, res) => {
40
+ console.error('Proxy error:', err.message);
41
+ if (res instanceof http.ServerResponse && !res.headersSent) {
42
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
43
+ res.end('Proxy error: ' + err.message);
44
+ }
45
+ });
46
+ }
18
47
  }
19
48
  static loadProxyConfig(configPath) {
20
49
  if (!fs.existsSync(configPath)) {
@@ -44,32 +73,57 @@ export class DevServer {
44
73
  }
45
74
  }
46
75
  static isProxyEntry(value) {
47
- return typeof value === 'object' &&
48
- value !== null &&
49
- 'target' in value &&
50
- typeof value.target === 'string';
76
+ return typeof value === 'object' && value !== null && 'target' in value && typeof value.target === 'string';
51
77
  }
52
78
  async start() {
53
79
  return new Promise((resolve, reject) => {
54
80
  this.server = http.createServer((req, res) => {
55
81
  this.handleRequest(req, res);
56
82
  });
83
+ this.wss = new WebSocketServer({ noServer: true });
84
+ this.server.on('upgrade', (req, socket, head) => {
85
+ if (req.url === '/_fluff/ws') {
86
+ this.wss?.handleUpgrade(req, socket, head, (ws) => {
87
+ this.wsClients.add(ws);
88
+ ws.on('close', () => {
89
+ this.wsClients.delete(ws);
90
+ });
91
+ });
92
+ }
93
+ else {
94
+ socket.destroy();
95
+ }
96
+ });
57
97
  this.server.on('error', (err) => {
58
98
  reject(err);
59
99
  });
60
- this.server.listen(this.options.port, () => {
100
+ this.server.listen(this.options.port, this.options.host, () => {
61
101
  const address = this.server?.address();
62
102
  const port = typeof address === 'object' && address ? address.port : this.options.port;
63
103
  resolve(port);
64
104
  });
65
105
  });
66
106
  }
107
+ notifyReload() {
108
+ const message = JSON.stringify({ type: 'reload' });
109
+ for (const client of this.wsClients) {
110
+ client.send(message);
111
+ }
112
+ }
67
113
  stop() {
114
+ if (this.wss) {
115
+ this.wss.close();
116
+ this.wss = null;
117
+ }
118
+ this.wsClients.clear();
68
119
  if (this.server) {
69
120
  this.server.close();
70
121
  this.server = null;
71
122
  }
72
- this.proxy.close();
123
+ if (this.proxy) {
124
+ this.proxy.close();
125
+ this.proxy = null;
126
+ }
73
127
  }
74
128
  handleRequest(req, res) {
75
129
  const url = req.url ?? '/';
@@ -79,7 +133,13 @@ export class DevServer {
79
133
  this.proxyRequest(req, res, proxyEntry);
80
134
  }
81
135
  else {
82
- this.forwardToEsbuild(req, res);
136
+ this.serveStatic(res, pathname).catch((err) => {
137
+ console.error('Static serve error:', err);
138
+ if (!res.headersSent) {
139
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
140
+ res.end('Internal Server Error');
141
+ }
142
+ });
83
143
  }
84
144
  }
85
145
  findProxyEntry(pathname) {
@@ -94,16 +154,35 @@ export class DevServer {
94
154
  return undefined;
95
155
  }
96
156
  proxyRequest(req, res, entry) {
157
+ if (!this.proxy) {
158
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
159
+ res.end('Proxy not configured');
160
+ return;
161
+ }
97
162
  const options = {
98
- target: entry.target,
99
- changeOrigin: entry.changeOrigin ?? false
163
+ target: entry.target, changeOrigin: entry.changeOrigin ?? false
100
164
  };
101
165
  this.proxy.web(req, res, options);
102
166
  }
103
- forwardToEsbuild(req, res) {
104
- const options = {
105
- target: `http://${this.options.esbuildHost}:${this.options.esbuildPort}`
106
- };
107
- this.proxy.web(req, res, options);
167
+ async serveStatic(res, pathname) {
168
+ const hasExtension = /\.\w+$/.test(pathname);
169
+ const resolvedPath = hasExtension ? path.resolve(this.options.outDir, pathname.replace(/^\/+/, '')) : path.resolve(this.options.outDir, 'index.html');
170
+ const resolvedOutDir = path.resolve(this.options.outDir);
171
+ if (!resolvedPath.startsWith(resolvedOutDir + path.sep)) {
172
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
173
+ res.end('403 - Forbidden');
174
+ return;
175
+ }
176
+ if (!fs.existsSync(resolvedPath)) {
177
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
178
+ res.end('404 - Not Found');
179
+ return;
180
+ }
181
+ const ext = path.extname(resolvedPath)
182
+ .toLowerCase();
183
+ const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
184
+ const content = await fs.promises.readFile(resolvedPath);
185
+ res.writeHead(200, { 'Content-Type': contentType });
186
+ res.end(content);
108
187
  }
109
188
  }
@@ -9,6 +9,16 @@ import { ExpressionTransformer } from './ExpressionTransformer.js';
9
9
  import { Parse5Helpers } from './Parse5Helpers.js';
10
10
  import { Typeguards } from './Typeguards.js';
11
11
  const RESTRICTED_ELEMENTS = ['select', 'table', 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th', 'colgroup'];
12
+ const voidElementCache = new Map();
13
+ function isVoidElement(tagName) {
14
+ let cached = voidElementCache.get(tagName);
15
+ if (cached === undefined) {
16
+ const fragment = parse5.parseFragment(`<${tagName} />`);
17
+ cached = parse5.serialize(fragment) === `<${tagName}>`;
18
+ voidElementCache.set(tagName, cached);
19
+ }
20
+ return cached;
21
+ }
12
22
  export class DomPreProcessor {
13
23
  stack = [];
14
24
  source = '';
@@ -96,7 +106,7 @@ export class DomPreProcessor {
96
106
  }
97
107
  const el = Parse5Helpers.createElement(tagName, attrs);
98
108
  this.appendNode(el);
99
- if (!token.selfClosing) {
109
+ if (!token.selfClosing && !isVoidElement(tagName)) {
100
110
  this.elementStack.push(el);
101
111
  }
102
112
  }
@@ -161,9 +171,7 @@ export class DomPreProcessor {
161
171
  buildBindingObject(name, binding, value, subscribeTo) {
162
172
  const parsed = ExpressionTransformer.parsePrimaryExpression(value);
163
173
  const bindingObj = {
164
- name,
165
- binding,
166
- expression: parsed.expression
174
+ name, binding, expression: parsed.expression
167
175
  };
168
176
  if (parsed.pipes.length > 0) {
169
177
  bindingObj.pipes = parsed.pipes;
@@ -313,9 +321,7 @@ export class DomPreProcessor {
313
321
  if (!this.rootFragment) {
314
322
  throw new Error('Internal error: parse5 root fragment not initialized');
315
323
  }
316
- const parent = this.elementStack.length > 0
317
- ? this.elementStack[this.elementStack.length - 1]
318
- : this.rootFragment;
324
+ const parent = this.elementStack.length > 0 ? this.elementStack[this.elementStack.length - 1] : this.rootFragment;
319
325
  node.parentNode = parent;
320
326
  parent.childNodes.push(node);
321
327
  }
@@ -323,9 +329,7 @@ export class DomPreProcessor {
323
329
  if (value.length === 0)
324
330
  return;
325
331
  const textNode = {
326
- nodeName: '#text',
327
- value,
328
- parentNode: null
332
+ nodeName: '#text', value, parentNode: null
329
333
  };
330
334
  this.appendNode(textNode);
331
335
  }
@@ -334,11 +338,7 @@ export class DomPreProcessor {
334
338
  throw new Error('Internal error: parse5 root fragment not initialized');
335
339
  }
336
340
  const doctypeNode = {
337
- nodeName: '#documentType',
338
- name,
339
- publicId: '',
340
- systemId: '',
341
- parentNode: null
341
+ nodeName: '#documentType', name, publicId: '', systemId: '', parentNode: null
342
342
  };
343
343
  doctypeNode.parentNode = this.rootFragment;
344
344
  this.rootFragment.childNodes.push(doctypeNode);
@@ -464,10 +464,7 @@ export class DomPreProcessor {
464
464
  return null;
465
465
  this.stack.push({ type: 'else' });
466
466
  return {
467
- tagName: 'x-fluff-else',
468
- attrs: [],
469
- endPos: bracePos + 1,
470
- opensBlock: true
467
+ tagName: 'x-fluff-else', attrs: [], endPos: bracePos + 1, opensBlock: true
471
468
  };
472
469
  }
473
470
  parseForStatement(text, startPos) {
@@ -490,17 +487,13 @@ export class DomPreProcessor {
490
487
  }
491
488
  this.stack.push({ type: 'for', iterator: result.iterator, iterable: result.iterable, trackBy: result.trackBy });
492
489
  const attrs = [
493
- { name: 'x-fluff-iterator', value: result.iterator },
494
- { name: 'x-fluff-iterable', value: result.iterable }
490
+ { name: 'x-fluff-iterator', value: result.iterator }, { name: 'x-fluff-iterable', value: result.iterable }
495
491
  ];
496
492
  if (result.trackBy) {
497
493
  attrs.push({ name: 'x-fluff-track', value: result.trackBy });
498
494
  }
499
495
  return {
500
- tagName: 'x-fluff-for',
501
- attrs,
502
- endPos: bracePos + 1,
503
- opensBlock: true
496
+ tagName: 'x-fluff-for', attrs, endPos: bracePos + 1, opensBlock: true
504
497
  };
505
498
  }
506
499
  parseSwitchStatement(text, startPos) {
@@ -533,10 +526,7 @@ export class DomPreProcessor {
533
526
  return null;
534
527
  this.stack.push({ type: 'default' });
535
528
  return {
536
- tagName: 'x-fluff-default',
537
- attrs: [],
538
- endPos: bracePos + 1,
539
- opensBlock: true
529
+ tagName: 'x-fluff-default', attrs: [], endPos: bracePos + 1, opensBlock: true
540
530
  };
541
531
  }
542
532
  parseEmptyStatement(text, startPos) {
@@ -545,18 +535,12 @@ export class DomPreProcessor {
545
535
  return null;
546
536
  this.stack.push({ type: 'empty' });
547
537
  return {
548
- tagName: 'x-fluff-empty',
549
- attrs: [],
550
- endPos: bracePos + 1,
551
- opensBlock: true
538
+ tagName: 'x-fluff-empty', attrs: [], endPos: bracePos + 1, opensBlock: true
552
539
  };
553
540
  }
554
541
  parseSimpleStatement(startPos, keywordLength, tagName) {
555
542
  return {
556
- tagName,
557
- attrs: [],
558
- endPos: startPos + 1 + keywordLength,
559
- opensBlock: false
543
+ tagName, attrs: [], endPos: startPos + 1 + keywordLength, opensBlock: false
560
544
  };
561
545
  }
562
546
  parseFallthroughStatement(text, startPos) {
@@ -610,9 +594,7 @@ export class DomPreProcessor {
610
594
  }
611
595
  handleComment(token) {
612
596
  const commentNode = {
613
- nodeName: '#comment',
614
- data: token.text,
615
- parentNode: null
597
+ nodeName: '#comment', data: token.text, parentNode: null
616
598
  };
617
599
  this.appendNode(commentNode);
618
600
  }