@fluffjs/cli 0.4.4 → 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.
- package/BabelHelpers.js +8 -5
- package/Cli.d.ts +9 -5
- package/Cli.js +218 -155
- package/CodeGenerator.d.ts +19 -10
- package/CodeGenerator.js +146 -106
- package/ComponentCompiler.d.ts +19 -3
- package/ComponentCompiler.js +175 -47
- package/DevServer.d.ts +6 -4
- package/DevServer.js +102 -23
- package/DomPreProcessor.js +22 -40
- package/IndexHtmlTransformer.js +20 -28
- package/PluginLoader.d.ts +22 -0
- package/PluginLoader.js +286 -0
- package/PluginManager.d.ts +39 -0
- package/PluginManager.js +209 -0
- package/TemplateParser.d.ts +5 -0
- package/TemplateParser.js +55 -0
- package/babel-plugin-directive.d.ts +12 -0
- package/babel-plugin-directive.js +78 -0
- package/babel-plugin-reactive.js +19 -14
- package/fluff-esbuild-plugin.js +66 -22
- package/interfaces/ClassTransformContext.d.ts +8 -0
- package/interfaces/ClassTransformContext.js +1 -0
- package/interfaces/CodeGenContext.d.ts +9 -0
- package/interfaces/CodeGenContext.js +1 -0
- package/interfaces/DiscoveryInfo.d.ts +15 -0
- package/interfaces/DiscoveryInfo.js +1 -0
- package/interfaces/EntryPointContext.d.ts +6 -0
- package/interfaces/EntryPointContext.js +1 -0
- package/interfaces/FluffConfigInterface.d.ts +2 -0
- package/interfaces/FluffPlugin.d.ts +26 -0
- package/interfaces/FluffPlugin.js +1 -0
- package/interfaces/FluffPluginOptions.d.ts +3 -0
- package/interfaces/FluffTarget.d.ts +1 -0
- package/interfaces/HtmlTransformOptions.d.ts +2 -0
- package/interfaces/PluginCustomTable.d.ts +6 -0
- package/interfaces/PluginCustomTable.js +1 -0
- package/interfaces/PluginHookDependency.d.ts +5 -0
- package/interfaces/PluginHookDependency.js +1 -0
- package/interfaces/PluginHookName.d.ts +2 -0
- package/interfaces/PluginHookName.js +1 -0
- package/interfaces/ScopeElementConfig.d.ts +5 -0
- package/interfaces/ScopeElementConfig.js +1 -0
- package/interfaces/index.d.ts +8 -0
- package/package.json +5 -3
- package/PeerDependencies.d.ts +0 -6
- package/PeerDependencies.js +0 -7
package/ComponentCompiler.js
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
return
|
|
32
|
+
getDirectivePaths() {
|
|
33
|
+
return [...this.directivePaths];
|
|
39
34
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
79
|
-
if (
|
|
80
|
-
this.componentSelectors.add(
|
|
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' })])
|
|
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
|
-
|
|
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')), [
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
res
|
|
15
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
}
|
package/DomPreProcessor.js
CHANGED
|
@@ -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
|
}
|