@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.
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,35 +5,26 @@ import { Parse5Helpers } from './Parse5Helpers.js';
5
5
  import { Typeguards } from './Typeguards.js';
6
6
  export class IndexHtmlTransformer {
7
7
  static LIVE_RELOAD_SCRIPT = `(function() {
8
- let firstEvent = true;
9
- const es = new EventSource('/esbuild');
10
- es.addEventListener('change', e => {
11
- if (firstEvent) {
12
- firstEvent = false;
13
- return;
14
- }
15
- const { added, removed, updated } = JSON.parse(e.data);
16
- if (!added.length && !removed.length && updated.length === 1) {
17
- for (const link of document.getElementsByTagName("link")) {
18
- const url = new URL(link.href);
19
- if (url.host === location.host && url.pathname === updated[0]) {
20
- const next = link.cloneNode();
21
- next.href = updated[0] + '?' + Math.random().toString(36).slice(2);
22
- next.onload = () => link.remove();
23
- link.parentNode.insertBefore(next, link.nextSibling);
24
- return;
25
- }
8
+ var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
9
+ var url = protocol + '//' + location.host + '/_fluff/ws';
10
+ function connect() {
11
+ var ws = new WebSocket(url);
12
+ ws.onmessage = function(e) {
13
+ var data = JSON.parse(e.data);
14
+ if (data.type === 'reload') {
15
+ location.reload();
26
16
  }
27
- }
28
- location.reload();
29
- });
17
+ };
18
+ ws.onclose = function() {
19
+ setTimeout(connect, 1000);
20
+ };
21
+ }
22
+ connect();
30
23
  })();`;
31
24
  static async transform(html, options) {
32
25
  const doc = parse5.parse(html);
33
26
  const jsSrc = options.gzScriptTag ? `${options.jsBundle}.gz` : options.jsBundle;
34
- const cssSrc = options.cssBundle
35
- ? (options.gzScriptTag ? `${options.cssBundle}.gz` : options.cssBundle)
36
- : null;
27
+ const cssSrc = options.cssBundle ? (options.gzScriptTag ? `${options.cssBundle}.gz` : options.cssBundle) : null;
37
28
  const head = Parse5Helpers.findElement(doc, 'head');
38
29
  const body = Parse5Helpers.findElement(doc, 'body');
39
30
  if (head && options.inlineStyles) {
@@ -43,15 +34,13 @@ export class IndexHtmlTransformer {
43
34
  }
44
35
  if (head && cssSrc) {
45
36
  const linkEl = Parse5Helpers.createElement('link', [
46
- { name: 'rel', value: 'stylesheet' },
47
- { name: 'href', value: cssSrc }
37
+ { name: 'rel', value: 'stylesheet' }, { name: 'href', value: cssSrc }
48
38
  ]);
49
39
  Parse5Helpers.appendChild(head, linkEl);
50
40
  }
51
41
  if (body) {
52
42
  const scriptEl = Parse5Helpers.createElement('script', [
53
- { name: 'type', value: 'module' },
54
- { name: 'src', value: jsSrc }
43
+ { name: 'type', value: 'module' }, { name: 'src', value: jsSrc }
55
44
  ]);
56
45
  Parse5Helpers.appendChild(body, scriptEl);
57
46
  }
@@ -63,6 +52,9 @@ export class IndexHtmlTransformer {
63
52
  if (options.liveReload) {
64
53
  IndexHtmlTransformer.decodeScriptTextNodes(doc);
65
54
  }
55
+ if (options.pluginManager?.hasHook('modifyIndexHtml')) {
56
+ await options.pluginManager.runModifyIndexHtml(doc);
57
+ }
66
58
  let result = parse5.serialize(doc);
67
59
  if (options.minify) {
68
60
  result = await minify(result, {
@@ -0,0 +1,22 @@
1
+ import type { FluffConfig } from './interfaces/FluffConfigInterface.js';
2
+ import { PluginManager } from './PluginManager.js';
3
+ export declare class PluginLoader {
4
+ static load(config: FluffConfig, projectRoot: string): Promise<PluginManager>;
5
+ private static loadPlugin;
6
+ private static resolvePluginPath;
7
+ private static tryResolveViaNx;
8
+ private static tryResolveNxManually;
9
+ private static tryResolveViaPackageJson;
10
+ private static tryResolveViaNodeModules;
11
+ private static tryResolveAsDirectPath;
12
+ private static tryResolveAsDirectory;
13
+ private static resolvePackageEntryPoint;
14
+ private static extractEntryFromPackageJson;
15
+ private static readPackageName;
16
+ private static readJsonFile;
17
+ private static safeReadDir;
18
+ private static findFileUpwards;
19
+ private static extractPlugin;
20
+ private static isFluffPlugin;
21
+ }
22
+ //# sourceMappingURL=PluginLoader.d.ts.map
@@ -0,0 +1,286 @@
1
+ import * as fs from 'fs';
2
+ import { createRequire } from 'module';
3
+ import * as path from 'path';
4
+ import { PluginManager } from './PluginManager.js';
5
+ import { Typeguards } from './Typeguards.js';
6
+ export class PluginLoader {
7
+ static async load(config, projectRoot) {
8
+ const manager = new PluginManager();
9
+ if (!config.plugins || config.plugins.length === 0) {
10
+ return manager;
11
+ }
12
+ for (const pluginSpec of config.plugins) {
13
+ const plugin = await PluginLoader.loadPlugin(pluginSpec, projectRoot);
14
+ manager.registerPlugin(plugin);
15
+ }
16
+ manager.resolveExecutionOrder();
17
+ return manager;
18
+ }
19
+ static async loadPlugin(spec, projectRoot) {
20
+ const modulePath = await PluginLoader.resolvePluginPath(spec, projectRoot);
21
+ const loaded = await import(modulePath).catch((e) => {
22
+ throw new Error(`Failed to load plugin '${spec}' from '${modulePath}': ${e instanceof Error ? e.message : String(e)}`);
23
+ });
24
+ const plugin = PluginLoader.extractPlugin(loaded);
25
+ if (!plugin) {
26
+ throw new Error(`Plugin '${spec}' does not export a valid FluffPlugin. ` +
27
+ 'Expected a default export with a \'name\' property, or a default export function that returns one.');
28
+ }
29
+ return plugin;
30
+ }
31
+ static async resolvePluginPath(spec, projectRoot) {
32
+ const absoluteProjectRoot = path.resolve(projectRoot);
33
+ const nxResult = await PluginLoader.tryResolveViaNx(spec, absoluteProjectRoot);
34
+ if (nxResult) {
35
+ return nxResult;
36
+ }
37
+ const packageJsonResult = PluginLoader.tryResolveViaPackageJson(spec, absoluteProjectRoot);
38
+ if (packageJsonResult) {
39
+ return packageJsonResult;
40
+ }
41
+ const nodeModulesResult = PluginLoader.tryResolveViaNodeModules(spec, absoluteProjectRoot);
42
+ if (nodeModulesResult) {
43
+ return nodeModulesResult;
44
+ }
45
+ const directPathResult = PluginLoader.tryResolveAsDirectPath(spec, absoluteProjectRoot);
46
+ if (directPathResult) {
47
+ return directPathResult;
48
+ }
49
+ const directoryResult = PluginLoader.tryResolveAsDirectory(spec, absoluteProjectRoot);
50
+ if (directoryResult) {
51
+ return directoryResult;
52
+ }
53
+ throw new Error(`Plugin '${spec}' not found. Searched:\n` +
54
+ ' - nx workspace packages\n' +
55
+ ' - package.json overrides/resolutions\n' +
56
+ ' - node_modules\n' +
57
+ ' - direct file path\n' +
58
+ ' - directory with package.json');
59
+ }
60
+ static async tryResolveViaNx(spec, projectRoot) {
61
+ const nxJsonPath = PluginLoader.findFileUpwards('nx.json', projectRoot);
62
+ if (!nxJsonPath) {
63
+ return null;
64
+ }
65
+ const workspaceRoot = path.dirname(nxJsonPath);
66
+ const manualResult = PluginLoader.tryResolveNxManually(spec, workspaceRoot);
67
+ if (manualResult) {
68
+ return manualResult;
69
+ }
70
+ try {
71
+ const devkit = await import('@nx/devkit');
72
+ const graph = await devkit.createProjectGraphAsync();
73
+ const projectNode = graph.nodes[spec];
74
+ if (!projectNode) {
75
+ return null;
76
+ }
77
+ const nxProjectRoot = path.resolve(workspaceRoot, projectNode.data.root);
78
+ return PluginLoader.resolvePackageEntryPoint(nxProjectRoot);
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ static tryResolveNxManually(spec, workspaceRoot) {
85
+ const searchDirs = ['packages', 'apps', 'libs'];
86
+ for (const searchDir of searchDirs) {
87
+ const dirPath = path.join(workspaceRoot, searchDir);
88
+ if (!fs.existsSync(dirPath)) {
89
+ continue;
90
+ }
91
+ const entries = PluginLoader.safeReadDir(dirPath);
92
+ if (!entries) {
93
+ continue;
94
+ }
95
+ for (const entry of entries) {
96
+ if (!entry.isDirectory()) {
97
+ continue;
98
+ }
99
+ const candidatePath = path.join(dirPath, entry.name);
100
+ const pkgJsonPath = path.join(candidatePath, 'package.json');
101
+ if (!fs.existsSync(pkgJsonPath)) {
102
+ continue;
103
+ }
104
+ const pkgName = PluginLoader.readPackageName(pkgJsonPath);
105
+ if (pkgName === spec) {
106
+ return PluginLoader.resolvePackageEntryPoint(candidatePath);
107
+ }
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ static tryResolveViaPackageJson(spec, projectRoot) {
113
+ const pkgJsonPath = PluginLoader.findFileUpwards('package.json', projectRoot);
114
+ if (!pkgJsonPath) {
115
+ return null;
116
+ }
117
+ const pkgJson = PluginLoader.readJsonFile(pkgJsonPath);
118
+ if (!pkgJson) {
119
+ return null;
120
+ }
121
+ const overridesVal = pkgJson.overrides;
122
+ const overrides = Typeguards.isRecord(overridesVal) ? overridesVal : undefined;
123
+ const resolutionsVal = pkgJson.resolutions;
124
+ const resolutions = Typeguards.isRecord(resolutionsVal) ? resolutionsVal : undefined;
125
+ const resolved = overrides?.[spec] ?? resolutions?.[spec];
126
+ if (typeof resolved !== 'string') {
127
+ return null;
128
+ }
129
+ if (resolved.startsWith('.') || resolved.startsWith('/')) {
130
+ const rootDir = path.dirname(pkgJsonPath);
131
+ const resolvedPath = path.resolve(rootDir, resolved);
132
+ if (fs.existsSync(resolvedPath)) {
133
+ if (fs.statSync(resolvedPath).isDirectory()) {
134
+ return PluginLoader.resolvePackageEntryPoint(resolvedPath);
135
+ }
136
+ return resolvedPath;
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+ static tryResolveViaNodeModules(spec, projectRoot) {
142
+ try {
143
+ const require = createRequire(path.join(projectRoot, 'noop.js'));
144
+ return require.resolve(spec);
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ static tryResolveAsDirectPath(spec, projectRoot) {
151
+ const resolved = path.isAbsolute(spec) ? spec : path.resolve(projectRoot, spec);
152
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
153
+ return resolved;
154
+ }
155
+ const extensions = ['.js', '.mjs', '.ts', '.mts'];
156
+ for (const ext of extensions) {
157
+ const withExt = resolved + ext;
158
+ if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) {
159
+ return withExt;
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+ static tryResolveAsDirectory(spec, projectRoot) {
165
+ const resolved = path.isAbsolute(spec) ? spec : path.resolve(projectRoot, spec);
166
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
167
+ return null;
168
+ }
169
+ return PluginLoader.resolvePackageEntryPoint(resolved);
170
+ }
171
+ static resolvePackageEntryPoint(dir) {
172
+ const pkgJsonPath = path.join(dir, 'package.json');
173
+ if (fs.existsSync(pkgJsonPath)) {
174
+ const pkgJson = PluginLoader.readJsonFile(pkgJsonPath);
175
+ if (!pkgJson) {
176
+ return null;
177
+ }
178
+ const entryPoint = PluginLoader.extractEntryFromPackageJson(pkgJson);
179
+ if (entryPoint) {
180
+ const fullPath = path.resolve(dir, entryPoint);
181
+ if (fs.existsSync(fullPath)) {
182
+ return fullPath;
183
+ }
184
+ }
185
+ }
186
+ const fallbacks = ['index.js', 'index.mjs', 'dist/index.js', 'dist/index.mjs'];
187
+ for (const fallback of fallbacks) {
188
+ const candidate = path.join(dir, fallback);
189
+ if (fs.existsSync(candidate)) {
190
+ return candidate;
191
+ }
192
+ }
193
+ return null;
194
+ }
195
+ static extractEntryFromPackageJson(pkgJson) {
196
+ const { exports } = pkgJson;
197
+ if (exports && typeof exports === 'object') {
198
+ if (Typeguards.isRecord(exports)) {
199
+ const dotExport = exports['.'];
200
+ if (typeof dotExport === 'string') {
201
+ return dotExport;
202
+ }
203
+ if (Typeguards.isRecord(dotExport)) {
204
+ if (typeof dotExport.import === 'string') {
205
+ return dotExport.import;
206
+ }
207
+ if (typeof dotExport.default === 'string') {
208
+ return dotExport.default;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ if (typeof pkgJson.module === 'string') {
214
+ return pkgJson.module;
215
+ }
216
+ if (typeof pkgJson.main === 'string') {
217
+ return pkgJson.main;
218
+ }
219
+ return null;
220
+ }
221
+ static readPackageName(pkgJsonPath) {
222
+ const content = PluginLoader.readJsonFile(pkgJsonPath);
223
+ if (!content) {
224
+ return null;
225
+ }
226
+ return typeof content.name === 'string' ? content.name : null;
227
+ }
228
+ static readJsonFile(filePath) {
229
+ try {
230
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
231
+ if (Typeguards.isRecord(raw)) {
232
+ return raw;
233
+ }
234
+ return null;
235
+ }
236
+ catch {
237
+ return null;
238
+ }
239
+ }
240
+ static safeReadDir(dirPath) {
241
+ try {
242
+ return fs.readdirSync(dirPath, { withFileTypes: true });
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ static findFileUpwards(filename, startDir) {
249
+ let dir = startDir;
250
+ while (dir !== path.dirname(dir)) {
251
+ const filePath = path.join(dir, filename);
252
+ if (fs.existsSync(filePath)) {
253
+ return filePath;
254
+ }
255
+ dir = path.dirname(dir);
256
+ }
257
+ return null;
258
+ }
259
+ static extractPlugin(loaded) {
260
+ if (!Typeguards.isRecord(loaded)) {
261
+ return null;
262
+ }
263
+ if ('default' in loaded) {
264
+ const defaultExport = loaded.default;
265
+ if (typeof defaultExport === 'function') {
266
+ const result = defaultExport();
267
+ if (PluginLoader.isFluffPlugin(result)) {
268
+ return result;
269
+ }
270
+ }
271
+ if (PluginLoader.isFluffPlugin(defaultExport)) {
272
+ return defaultExport;
273
+ }
274
+ }
275
+ if (PluginLoader.isFluffPlugin(loaded)) {
276
+ return loaded;
277
+ }
278
+ return null;
279
+ }
280
+ static isFluffPlugin(value) {
281
+ return value !== null
282
+ && typeof value === 'object'
283
+ && 'name' in value
284
+ && typeof value.name === 'string';
285
+ }
286
+ }
@@ -0,0 +1,39 @@
1
+ import type * as parse5 from 'parse5';
2
+ import type { ClassTransformContext } from './interfaces/ClassTransformContext.js';
3
+ import type { CodeGenContext } from './interfaces/CodeGenContext.js';
4
+ import type { DiscoveryInfo } from './interfaces/DiscoveryInfo.js';
5
+ import type { EntryPointContext } from './interfaces/EntryPointContext.js';
6
+ import type { FluffConfig } from './interfaces/FluffConfigInterface.js';
7
+ import type { FluffPlugin } from './interfaces/FluffPlugin.js';
8
+ import type { ParsedTemplate } from './interfaces/ParsedTemplate.js';
9
+ import type { PluginCustomTable } from './interfaces/PluginCustomTable.js';
10
+ import type { PluginHookName } from './interfaces/PluginHookName.js';
11
+ import type { ScopeElementConfig } from './interfaces/ScopeElementConfig.js';
12
+ export declare class PluginManager {
13
+ private readonly plugins;
14
+ private readonly pluginsByName;
15
+ private readonly hookExecutionOrder;
16
+ registerPlugin(plugin: FluffPlugin): void;
17
+ resolveExecutionOrder(): void;
18
+ private resolveHookOrder;
19
+ private parseDependency;
20
+ private topologicalSort;
21
+ private getOrderedEntries;
22
+ runAfterConfig(config: FluffConfig, pluginConfigs: Record<string, Record<string, unknown>>): Promise<void>;
23
+ runAfterDiscovery(discovery: DiscoveryInfo): Promise<void>;
24
+ runBeforeTemplatePreProcess(fragment: parse5.DefaultTreeAdapterMap['documentFragment'], componentSelector: string): Promise<void>;
25
+ runAfterTemplateParse(template: ParsedTemplate, componentSelector: string): Promise<void>;
26
+ runAfterCodeGeneration(context: CodeGenContext): Promise<void>;
27
+ runBeforeClassTransform(context: ClassTransformContext): Promise<void>;
28
+ runModifyEntryPoint(context: EntryPointContext): Promise<void>;
29
+ runModifyIndexHtml(document: parse5.DefaultTreeAdapterMap['document']): Promise<void>;
30
+ private runHook;
31
+ collectRuntimeImports(): string[];
32
+ collectCustomTables(): PluginCustomTable[];
33
+ collectScopeElements(): ScopeElementConfig[];
34
+ internString(value: string): number;
35
+ internExpression(expr: string): number;
36
+ get hasPlugins(): boolean;
37
+ hasHook(hookName: PluginHookName): boolean;
38
+ }
39
+ //# sourceMappingURL=PluginManager.d.ts.map
@@ -0,0 +1,209 @@
1
+ import { CodeGenerator } from './CodeGenerator.js';
2
+ export class PluginManager {
3
+ plugins = [];
4
+ pluginsByName = new Map();
5
+ hookExecutionOrder = new Map();
6
+ registerPlugin(plugin) {
7
+ if (this.pluginsByName.has(plugin.name)) {
8
+ throw new Error(`Plugin '${plugin.name}' is already registered`);
9
+ }
10
+ this.plugins.push(plugin);
11
+ this.pluginsByName.set(plugin.name, plugin);
12
+ }
13
+ resolveExecutionOrder() {
14
+ this.hookExecutionOrder.clear();
15
+ const allHookNames = [
16
+ 'afterConfig',
17
+ 'afterDiscovery',
18
+ 'beforeTemplatePreProcess',
19
+ 'afterTemplateParse',
20
+ 'afterCodeGeneration',
21
+ 'beforeClassTransform',
22
+ 'modifyEntryPoint',
23
+ 'modifyIndexHtml',
24
+ 'registerRuntimeImports',
25
+ 'registerCustomTables',
26
+ 'registerScopeElements'
27
+ ];
28
+ for (const hookName of allHookNames) {
29
+ const entries = this.resolveHookOrder(hookName);
30
+ this.hookExecutionOrder.set(hookName, entries);
31
+ }
32
+ }
33
+ resolveHookOrder(hookName) {
34
+ const participants = [];
35
+ for (const plugin of this.plugins) {
36
+ if (plugin[hookName]) {
37
+ participants.push({ pluginName: plugin.name, hookName, plugin });
38
+ }
39
+ }
40
+ if (participants.length <= 1) {
41
+ return participants;
42
+ }
43
+ const graph = new Map();
44
+ for (const entry of participants) {
45
+ graph.set(entry.pluginName, new Set());
46
+ }
47
+ for (const entry of participants) {
48
+ const deps = entry.plugin.dependencies?.[hookName];
49
+ if (!deps) {
50
+ continue;
51
+ }
52
+ for (const depSpec of deps) {
53
+ const parsed = this.parseDependency(depSpec);
54
+ if (parsed.pluginName && parsed.hookName) {
55
+ if (parsed.hookName === hookName && graph.has(parsed.pluginName)) {
56
+ graph.get(entry.pluginName)?.add(parsed.pluginName);
57
+ }
58
+ }
59
+ else if (parsed.pluginName && !parsed.hookName) {
60
+ if (graph.has(parsed.pluginName)) {
61
+ graph.get(entry.pluginName)?.add(parsed.pluginName);
62
+ }
63
+ }
64
+ else if (!parsed.pluginName && parsed.hookName) {
65
+ if (parsed.hookName === hookName) {
66
+ for (const other of participants) {
67
+ if (other.pluginName !== entry.pluginName) {
68
+ graph.get(entry.pluginName)?.add(other.pluginName);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return this.topologicalSort(participants, graph);
76
+ }
77
+ parseDependency(spec) {
78
+ if (spec.startsWith(':')) {
79
+ return { hookName: spec.slice(1) };
80
+ }
81
+ const colonIndex = spec.indexOf(':');
82
+ if (colonIndex === -1) {
83
+ return { pluginName: spec };
84
+ }
85
+ return {
86
+ pluginName: spec.slice(0, colonIndex),
87
+ hookName: spec.slice(colonIndex + 1)
88
+ };
89
+ }
90
+ topologicalSort(entries, graph) {
91
+ const visited = new Set();
92
+ const visiting = new Set();
93
+ const sorted = [];
94
+ const visit = (name) => {
95
+ if (visited.has(name)) {
96
+ return;
97
+ }
98
+ if (visiting.has(name)) {
99
+ const cycle = Array.from(visiting).join(' -> ') + ' -> ' + name;
100
+ throw new Error(`Circular plugin dependency detected: ${cycle}`);
101
+ }
102
+ visiting.add(name);
103
+ const deps = graph.get(name);
104
+ if (deps) {
105
+ for (const dep of deps) {
106
+ visit(dep);
107
+ }
108
+ }
109
+ visiting.delete(name);
110
+ visited.add(name);
111
+ sorted.push(name);
112
+ };
113
+ for (const entry of entries) {
114
+ visit(entry.pluginName);
115
+ }
116
+ const entryMap = new Map();
117
+ for (const entry of entries) {
118
+ entryMap.set(entry.pluginName, entry);
119
+ }
120
+ const result = [];
121
+ for (const name of sorted) {
122
+ const entry = entryMap.get(name);
123
+ if (entry) {
124
+ result.push(entry);
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ getOrderedEntries(hookName) {
130
+ return this.hookExecutionOrder.get(hookName) ?? [];
131
+ }
132
+ async runAfterConfig(config, pluginConfigs) {
133
+ for (const entry of this.getOrderedEntries('afterConfig')) {
134
+ const pluginConfig = pluginConfigs[entry.pluginName] ?? {};
135
+ const hook = entry.plugin.afterConfig;
136
+ if (hook) {
137
+ await hook.call(entry.plugin, config, pluginConfig);
138
+ }
139
+ }
140
+ }
141
+ async runAfterDiscovery(discovery) {
142
+ await this.runHook('afterDiscovery', async (plugin) => plugin.afterDiscovery?.(discovery));
143
+ }
144
+ async runBeforeTemplatePreProcess(fragment, componentSelector) {
145
+ await this.runHook('beforeTemplatePreProcess', async (plugin) => plugin.beforeTemplatePreProcess?.(fragment, componentSelector));
146
+ }
147
+ async runAfterTemplateParse(template, componentSelector) {
148
+ await this.runHook('afterTemplateParse', async (plugin) => plugin.afterTemplateParse?.(template, componentSelector));
149
+ }
150
+ async runAfterCodeGeneration(context) {
151
+ await this.runHook('afterCodeGeneration', async (plugin) => plugin.afterCodeGeneration?.(context));
152
+ }
153
+ async runBeforeClassTransform(context) {
154
+ await this.runHook('beforeClassTransform', async (plugin) => plugin.beforeClassTransform?.(context));
155
+ }
156
+ async runModifyEntryPoint(context) {
157
+ await this.runHook('modifyEntryPoint', async (plugin) => plugin.modifyEntryPoint?.(context));
158
+ }
159
+ async runModifyIndexHtml(document) {
160
+ await this.runHook('modifyIndexHtml', async (plugin) => plugin.modifyIndexHtml?.(document));
161
+ }
162
+ async runHook(hookName, invoke) {
163
+ for (const entry of this.getOrderedEntries(hookName)) {
164
+ await invoke(entry.plugin);
165
+ }
166
+ }
167
+ collectRuntimeImports() {
168
+ const imports = [];
169
+ for (const entry of this.getOrderedEntries('registerRuntimeImports')) {
170
+ const hook = entry.plugin.registerRuntimeImports;
171
+ if (hook) {
172
+ imports.push(...hook.call(entry.plugin));
173
+ }
174
+ }
175
+ return imports;
176
+ }
177
+ collectCustomTables() {
178
+ const tables = [];
179
+ for (const entry of this.getOrderedEntries('registerCustomTables')) {
180
+ const hook = entry.plugin.registerCustomTables;
181
+ if (hook) {
182
+ tables.push(...hook.call(entry.plugin));
183
+ }
184
+ }
185
+ return tables;
186
+ }
187
+ collectScopeElements() {
188
+ const elements = [];
189
+ for (const entry of this.getOrderedEntries('registerScopeElements')) {
190
+ const hook = entry.plugin.registerScopeElements;
191
+ if (hook) {
192
+ elements.push(...hook.call(entry.plugin));
193
+ }
194
+ }
195
+ return elements;
196
+ }
197
+ internString(value) {
198
+ return CodeGenerator.internString(value);
199
+ }
200
+ internExpression(expr) {
201
+ return CodeGenerator.internExpression(expr);
202
+ }
203
+ get hasPlugins() {
204
+ return this.plugins.length > 0;
205
+ }
206
+ hasHook(hookName) {
207
+ return (this.hookExecutionOrder.get(hookName)?.length ?? 0) > 0;
208
+ }
209
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ParsedTemplate } from './interfaces/ParsedTemplate.js';
2
+ import type { ScopeElementConfig } from './interfaces/ScopeElementConfig.js';
2
3
  export type { BindingInfo } from './interfaces/BindingInfo.js';
3
4
  export type { BreakNode } from './interfaces/BreakNode.js';
4
5
  export type { CommentNode } from './interfaces/CommentNode.js';
@@ -20,8 +21,10 @@ export declare class TemplateParser {
20
21
  private templateRefs;
21
22
  private scopeStack;
22
23
  private getterDependencyMap;
24
+ private scopeElements;
23
25
  private testYieldBeforeGetterDepsLookup;
24
26
  setGetterDependencyMap(map: Map<string, string[]>): void;
27
+ setScopeElements(configs: readonly ScopeElementConfig[]): void;
25
28
  __setTestYieldBeforeGetterDepsLookup(callback: (() => Promise<void>) | null): void;
26
29
  parse(html: string): Promise<ParsedTemplate>;
27
30
  private processNodes;
@@ -29,6 +32,8 @@ export declare class TemplateParser {
29
32
  private processConsolidatedFor;
30
33
  private processNode;
31
34
  private processElement;
35
+ private findScopeElementConfig;
36
+ private processScopedElement;
32
37
  private processForElement;
33
38
  private processSwitchElement;
34
39
  private hasChildElement;