@hyperfixi/vite-plugin 2.0.0 → 2.1.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/dist/index.cjs CHANGED
@@ -30,11 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DomainAggregator: () => DomainAggregator,
34
+ DomainScanner: () => DomainScanner,
33
35
  REGIONS: () => REGIONS,
34
36
  SUPPORTED_LANGUAGES: () => SUPPORTED_LANGUAGES,
35
37
  clearCustomKeywords: () => clearCustomKeywords,
36
38
  clearSemanticParser: () => clearSemanticParser,
37
39
  default: () => index_default,
40
+ descriptorToScanRule: () => descriptorToScanRule,
38
41
  detectLanguages: () => detectLanguages,
39
42
  getAllLanguageCodes: () => getAllLanguageCodes,
40
43
  getKeywordsForLanguage: () => getKeywordsForLanguage,
@@ -522,6 +525,8 @@ var POLISH_KEYWORDS = /* @__PURE__ */ new Set([
522
525
  "zwi\u0119ksz",
523
526
  "zwieksz",
524
527
  "zmniejsz",
528
+ "uzyskaj",
529
+ "pobierz",
525
530
  "wywo\u0142aj",
526
531
  "wywolaj",
527
532
  "wyzw\xF3l",
@@ -1861,6 +1866,15 @@ async function evaluate(node, ctx) {
1861
1866
  const obj = await evaluate(node.object, ctx);
1862
1867
  if (obj == null) return undefined;
1863
1868
  const prop = node.computed ? await evaluate(node.property, ctx) : node.property;
1869
+ if (prop === 'values' && obj instanceof Element) {
1870
+ if (obj instanceof HTMLFormElement) return new FormData(obj);
1871
+ const fd = new FormData();
1872
+ obj.querySelectorAll('input, select, textarea').forEach(input => {
1873
+ const name = input.getAttribute('name');
1874
+ if (name && 'value' in input) fd.append(name, input.value);
1875
+ });
1876
+ return fd;
1877
+ }
1864
1878
  return obj[prop];
1865
1879
 
1866
1880
  case 'call': {
@@ -2945,6 +2959,9 @@ function compileExpression(node) {
2945
2959
  if (prop.startsWith("*")) {
2946
2960
  return `${obj}.style.${prop.slice(1)}`;
2947
2961
  }
2962
+ if (prop === "values") {
2963
+ return `(function(el){if(el instanceof HTMLFormElement)return new FormData(el);var fd=new FormData();el.querySelectorAll('input,select,textarea').forEach(function(i){var n=i.getAttribute('name');if(n&&'value'in i)fd.append(n,i.value)});return fd})(${obj})`;
2964
+ }
2948
2965
  return `${obj}.${prop}`;
2949
2966
  }
2950
2967
  case "member": {
@@ -3268,6 +3285,290 @@ function extractScripts(html) {
3268
3285
  return scripts;
3269
3286
  }
3270
3287
 
3288
+ // src/server-bridge-integration.ts
3289
+ var import_node_path = require("path");
3290
+ var import_promises = require("fs/promises");
3291
+ var serverBridge = null;
3292
+ var loadAttempted = false;
3293
+ var lastRouteHash = "";
3294
+ async function loadServerBridge(debug) {
3295
+ if (loadAttempted) return serverBridge;
3296
+ loadAttempted = true;
3297
+ try {
3298
+ const mod = await import("@hyperfixi/server-bridge");
3299
+ serverBridge = mod;
3300
+ if (debug) {
3301
+ console.log("[hyperfixi] Server-bridge loaded successfully");
3302
+ }
3303
+ return serverBridge;
3304
+ } catch {
3305
+ console.warn(
3306
+ "[hyperfixi] @hyperfixi/server-bridge is not installed. Install it to enable server route generation: npm install @hyperfixi/server-bridge"
3307
+ );
3308
+ return null;
3309
+ }
3310
+ }
3311
+ function computeRouteHash(routes) {
3312
+ return routes.map((r) => `${r.method}:${r.path}`).sort().join("|");
3313
+ }
3314
+ async function runServerBridge(cwd, options, debug, force) {
3315
+ const mod = serverBridge ?? await loadServerBridge(debug);
3316
+ if (!mod) return;
3317
+ try {
3318
+ const outputDir = (0, import_node_path.resolve)(cwd, options.output ?? "./server/routes");
3319
+ const result = await mod.generate({
3320
+ dir: cwd,
3321
+ framework: options.framework ?? "express",
3322
+ output: options.output,
3323
+ typescript: options.typescript,
3324
+ ignore: options.ignore
3325
+ });
3326
+ const routeHash = computeRouteHash(result.routes);
3327
+ if (!force && routeHash === lastRouteHash) {
3328
+ if (debug) {
3329
+ console.log("[hyperfixi] Server-bridge: routes unchanged, skipping generation");
3330
+ }
3331
+ return;
3332
+ }
3333
+ lastRouteHash = routeHash;
3334
+ if (result.routes.length === 0) {
3335
+ if (debug) {
3336
+ console.log("[hyperfixi] Server-bridge: no routes found");
3337
+ }
3338
+ return;
3339
+ }
3340
+ await (0, import_promises.mkdir)(outputDir, { recursive: true });
3341
+ for (const file of result.generated.files) {
3342
+ const filePath = (0, import_node_path.join)(outputDir, file.path);
3343
+ const fileDir = (0, import_node_path.join)(filePath, "..");
3344
+ await (0, import_promises.mkdir)(fileDir, { recursive: true });
3345
+ await (0, import_promises.writeFile)(filePath, file.content, "utf-8");
3346
+ }
3347
+ const manifest = mod.createManifest(result.routes);
3348
+ await mod.saveManifest(outputDir, manifest);
3349
+ if (debug) {
3350
+ console.log(
3351
+ `[hyperfixi] Server-bridge: generated ${result.generated.files.length} file(s) for ${result.routes.length} route(s) in ${outputDir}/`
3352
+ );
3353
+ }
3354
+ if (result.scan.conflicts.length > 0) {
3355
+ console.warn(
3356
+ `[hyperfixi] Server-bridge: ${result.scan.conflicts.length} route conflict(s) detected`
3357
+ );
3358
+ }
3359
+ } catch (err) {
3360
+ console.error(
3361
+ "[hyperfixi] Server-bridge generation failed:",
3362
+ err instanceof Error ? err.message : err
3363
+ );
3364
+ }
3365
+ }
3366
+
3367
+ // src/domain-scanner.ts
3368
+ function buildAttributePatterns(attributes) {
3369
+ const patterns = [];
3370
+ for (const attr of attributes) {
3371
+ const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3372
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*"([^"]+)"`, "g"));
3373
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*'([^']+)'`, "g"));
3374
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*\`([^\`]+)\``, "g"));
3375
+ }
3376
+ return patterns;
3377
+ }
3378
+ function buildScriptPatterns(scriptTypes) {
3379
+ return scriptTypes.map((type) => {
3380
+ const escaped = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3381
+ return new RegExp(`<script[^>]*type=["']?${escaped}["']?[^>]*>([\\s\\S]*?)<\\/script>`, "gi");
3382
+ });
3383
+ }
3384
+ function detectKeywords(snippet, keywords) {
3385
+ const detectedKeywords = /* @__PURE__ */ new Set();
3386
+ const detectedLanguages = /* @__PURE__ */ new Set();
3387
+ for (const [lang, kwList] of Object.entries(keywords)) {
3388
+ for (const kw of kwList) {
3389
+ const isLatin = /^[a-zA-Z]/.test(kw);
3390
+ const found = isLatin ? new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(snippet) : snippet.includes(kw);
3391
+ if (found) {
3392
+ detectedKeywords.add(kw);
3393
+ detectedLanguages.add(lang);
3394
+ }
3395
+ }
3396
+ }
3397
+ return { detectedKeywords, detectedLanguages };
3398
+ }
3399
+ var DomainScanner = class {
3400
+ constructor(rules) {
3401
+ this.rules = rules;
3402
+ const allAttrs = rules.flatMap((r) => [...r.attributes]);
3403
+ const allScriptTypes = rules.flatMap((r) => r.scriptTypes ? [...r.scriptTypes] : []);
3404
+ const parts = [];
3405
+ for (const attr of allAttrs) {
3406
+ parts.push(attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3407
+ }
3408
+ for (const type of allScriptTypes) {
3409
+ parts.push(type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3410
+ }
3411
+ this.quickCheckPattern = parts.length > 0 ? new RegExp(parts.join("|"), "i") : null;
3412
+ }
3413
+ /**
3414
+ * Fast pre-check: does this code contain any domain-related attributes?
3415
+ */
3416
+ hasAnyDomainUsage(code) {
3417
+ if (!this.quickCheckPattern) return false;
3418
+ return this.quickCheckPattern.test(code);
3419
+ }
3420
+ /**
3421
+ * Scan a file for all registered domain syntaxes.
3422
+ * Returns one DomainFileUsage per domain that had matches.
3423
+ */
3424
+ scan(code, _id) {
3425
+ const results = [];
3426
+ for (const rule of this.rules) {
3427
+ const snippets = [];
3428
+ const attrPatterns = buildAttributePatterns(rule.attributes);
3429
+ for (const pattern of attrPatterns) {
3430
+ pattern.lastIndex = 0;
3431
+ let match;
3432
+ while (match = pattern.exec(code)) {
3433
+ const value = match[1].trim();
3434
+ if (value) snippets.push(value);
3435
+ }
3436
+ }
3437
+ if (rule.scriptTypes) {
3438
+ const scriptPatterns = buildScriptPatterns(rule.scriptTypes);
3439
+ for (const pattern of scriptPatterns) {
3440
+ pattern.lastIndex = 0;
3441
+ let match;
3442
+ while (match = pattern.exec(code)) {
3443
+ const value = match[1].trim();
3444
+ if (value) snippets.push(value);
3445
+ }
3446
+ }
3447
+ }
3448
+ if (snippets.length === 0) continue;
3449
+ const allDetectedKeywords = /* @__PURE__ */ new Set();
3450
+ const allDetectedLanguages = /* @__PURE__ */ new Set();
3451
+ if (rule.keywords) {
3452
+ for (const snippet of snippets) {
3453
+ const { detectedKeywords, detectedLanguages } = detectKeywords(snippet, rule.keywords);
3454
+ for (const kw of detectedKeywords) allDetectedKeywords.add(kw);
3455
+ for (const lang of detectedLanguages) allDetectedLanguages.add(lang);
3456
+ }
3457
+ }
3458
+ results.push({
3459
+ domain: rule.domain,
3460
+ detectedKeywords: allDetectedKeywords,
3461
+ detectedLanguages: allDetectedLanguages,
3462
+ snippetCount: snippets.length
3463
+ });
3464
+ }
3465
+ return results;
3466
+ }
3467
+ };
3468
+ function descriptorToScanRule(descriptor) {
3469
+ if (!descriptor.scanConfig) return null;
3470
+ return {
3471
+ domain: descriptor.name,
3472
+ attributes: [...descriptor.scanConfig.attributes],
3473
+ scriptTypes: descriptor.scanConfig.scriptTypes ? [...descriptor.scanConfig.scriptTypes] : void 0,
3474
+ keywords: descriptor.scanConfig.keywords ? Object.fromEntries(
3475
+ Object.entries(descriptor.scanConfig.keywords).map(([k, v]) => [k, [...v]])
3476
+ ) : void 0
3477
+ };
3478
+ }
3479
+
3480
+ // src/domain-aggregator.ts
3481
+ var DomainAggregator = class {
3482
+ constructor() {
3483
+ this.fileUsage = /* @__PURE__ */ new Map();
3484
+ this.cache = null;
3485
+ }
3486
+ /**
3487
+ * Add or update domain usage for a file.
3488
+ * Returns true if usage changed (invalidates cache).
3489
+ */
3490
+ add(filePath, usage) {
3491
+ const existing = this.fileUsage.get(filePath);
3492
+ const changed = !existing || !this.isEqual(existing, usage);
3493
+ if (changed) {
3494
+ this.fileUsage.set(filePath, usage);
3495
+ this.cache = null;
3496
+ }
3497
+ return changed;
3498
+ }
3499
+ /**
3500
+ * Remove a file's domain usage.
3501
+ * Returns true if the file was tracked.
3502
+ */
3503
+ remove(filePath) {
3504
+ const had = this.fileUsage.has(filePath);
3505
+ if (had) {
3506
+ this.fileUsage.delete(filePath);
3507
+ this.cache = null;
3508
+ }
3509
+ return had;
3510
+ }
3511
+ /**
3512
+ * Get aggregated usage across all files.
3513
+ * Result is cached until add/remove changes it.
3514
+ */
3515
+ getUsage() {
3516
+ if (this.cache) return this.cache;
3517
+ const domainMap = /* @__PURE__ */ new Map();
3518
+ for (const usages of this.fileUsage.values()) {
3519
+ for (const usage of usages) {
3520
+ let entry = domainMap.get(usage.domain);
3521
+ if (!entry) {
3522
+ entry = {
3523
+ detectedKeywords: /* @__PURE__ */ new Set(),
3524
+ detectedLanguages: /* @__PURE__ */ new Set(),
3525
+ fileCount: 0,
3526
+ totalSnippets: 0
3527
+ };
3528
+ domainMap.set(usage.domain, entry);
3529
+ }
3530
+ entry.fileCount++;
3531
+ entry.totalSnippets += usage.snippetCount;
3532
+ for (const kw of usage.detectedKeywords) entry.detectedKeywords.add(kw);
3533
+ for (const lang of usage.detectedLanguages) entry.detectedLanguages.add(lang);
3534
+ }
3535
+ }
3536
+ this.cache = Array.from(domainMap.entries()).map(([domain, data]) => ({
3537
+ domain,
3538
+ detectedKeywords: data.detectedKeywords,
3539
+ detectedLanguages: data.detectedLanguages,
3540
+ fileCount: data.fileCount,
3541
+ totalSnippets: data.totalSnippets
3542
+ }));
3543
+ return this.cache;
3544
+ }
3545
+ /**
3546
+ * Get aggregated usage for a specific domain.
3547
+ */
3548
+ getUsageForDomain(domain) {
3549
+ return this.getUsage().find((u) => u.domain === domain) ?? null;
3550
+ }
3551
+ /**
3552
+ * Clear all tracked data.
3553
+ */
3554
+ clear() {
3555
+ this.fileUsage.clear();
3556
+ this.cache = null;
3557
+ }
3558
+ // ---------------------------------------------------------------------------
3559
+ // Private
3560
+ // ---------------------------------------------------------------------------
3561
+ isEqual(a, b) {
3562
+ if (a.length !== b.length) return false;
3563
+ for (let i = 0; i < a.length; i++) {
3564
+ if (a[i].domain !== b[i].domain) return false;
3565
+ if (a[i].snippetCount !== b[i].snippetCount) return false;
3566
+ if (a[i].detectedKeywords.size !== b[i].detectedKeywords.size) return false;
3567
+ }
3568
+ return true;
3569
+ }
3570
+ };
3571
+
3271
3572
  // src/index.ts
3272
3573
  var VIRTUAL_MODULE_ID = "virtual:hyperfixi";
3273
3574
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
@@ -3277,6 +3578,8 @@ function hyperfixi(options = {}) {
3277
3578
  const scanner = new Scanner(options);
3278
3579
  const aggregator = new Aggregator();
3279
3580
  const generator = new Generator(options);
3581
+ const domainScanner = options.domains && options.domains.length > 0 ? new DomainScanner(options.domains) : null;
3582
+ const domainAggregator = domainScanner ? new DomainAggregator() : null;
3280
3583
  let server = null;
3281
3584
  let cachedBundle = null;
3282
3585
  let lastUsageHash = "";
@@ -3379,11 +3682,14 @@ function hyperfixi(options = {}) {
3379
3682
  /**
3380
3683
  * Configure plugin based on Vite mode
3381
3684
  */
3382
- configResolved(config) {
3685
+ async configResolved(config) {
3383
3686
  isDev = config.command === "serve";
3384
3687
  if (options.debug) {
3385
3688
  console.log("[hyperfixi] Plugin initialized, mode:", isDev ? "development" : "production");
3386
3689
  }
3690
+ if (options.serverBridge) {
3691
+ await loadServerBridge(options.debug);
3692
+ }
3387
3693
  },
3388
3694
  /**
3389
3695
  * Store server reference and pre-scan HTML files for HMR
@@ -3396,6 +3702,25 @@ function hyperfixi(options = {}) {
3396
3702
  }
3397
3703
  const scannedFiles = await scanner.scanProject(cwd);
3398
3704
  aggregator.loadFromScan(scannedFiles);
3705
+ if (domainScanner && domainAggregator) {
3706
+ for (const [filePath] of scannedFiles) {
3707
+ try {
3708
+ const fs = await import("fs");
3709
+ const content = fs.readFileSync(filePath, "utf-8");
3710
+ if (domainScanner.hasAnyDomainUsage(content)) {
3711
+ const domainUsages = domainScanner.scan(content, filePath);
3712
+ if (domainUsages.length > 0) {
3713
+ domainAggregator.add(filePath, domainUsages);
3714
+ }
3715
+ }
3716
+ } catch {
3717
+ }
3718
+ }
3719
+ if (options.debug) {
3720
+ const domainSummary = domainAggregator.getUsage();
3721
+ console.log("[hyperfixi] Domain pre-scan:", domainSummary);
3722
+ }
3723
+ }
3399
3724
  if (options.debug) {
3400
3725
  const summary = aggregator.getSummary();
3401
3726
  console.log("[hyperfixi] Pre-scan complete:", summary);
@@ -3442,6 +3767,12 @@ function hyperfixi(options = {}) {
3442
3767
  }
3443
3768
  const usage = scanner.scan(code, id);
3444
3769
  const changed = aggregator.add(id, usage);
3770
+ if (domainScanner && domainAggregator && domainScanner.hasAnyDomainUsage(code)) {
3771
+ const domainUsages = domainScanner.scan(code, id);
3772
+ if (domainUsages.length > 0) {
3773
+ domainAggregator.add(id, domainUsages);
3774
+ }
3775
+ }
3445
3776
  if (changed && server) {
3446
3777
  setImmediate(() => {
3447
3778
  const currentHash = computeUsageHash(aggregator.getUsage());
@@ -3471,6 +3802,9 @@ function hyperfixi(options = {}) {
3471
3802
  invalidateVirtualModule();
3472
3803
  }
3473
3804
  }
3805
+ if (options.serverBridge && server) {
3806
+ await runServerBridge(server.config.root, options.serverBridge, options.debug);
3807
+ }
3474
3808
  } catch {
3475
3809
  if (aggregator.remove(file)) {
3476
3810
  invalidateVirtualModule();
@@ -3501,6 +3835,25 @@ function hyperfixi(options = {}) {
3501
3835
  }
3502
3836
  const scannedFiles = await scanner.scanProject(cwd);
3503
3837
  aggregator.loadFromScan(scannedFiles);
3838
+ if (domainScanner && domainAggregator) {
3839
+ for (const [filePath] of scannedFiles) {
3840
+ try {
3841
+ const fs = await import("fs");
3842
+ const content = fs.readFileSync(filePath, "utf-8");
3843
+ if (domainScanner.hasAnyDomainUsage(content)) {
3844
+ const domainUsages = domainScanner.scan(content, filePath);
3845
+ if (domainUsages.length > 0) {
3846
+ domainAggregator.add(filePath, domainUsages);
3847
+ }
3848
+ }
3849
+ } catch {
3850
+ }
3851
+ }
3852
+ if (options.debug) {
3853
+ const domainSummary = domainAggregator.getUsage();
3854
+ console.log("[hyperfixi] Domain build scan:", domainSummary);
3855
+ }
3856
+ }
3504
3857
  if (mode === "compile") {
3505
3858
  const usage = aggregator.getUsage();
3506
3859
  if (usage.detectedLanguages.size > 0) {
@@ -3526,6 +3879,9 @@ function hyperfixi(options = {}) {
3526
3879
  languages: summary.languages
3527
3880
  });
3528
3881
  }
3882
+ if (options.serverBridge) {
3883
+ await runServerBridge(cwd, options.serverBridge, options.debug, true);
3884
+ }
3529
3885
  }
3530
3886
  }
3531
3887
  };
@@ -3533,10 +3889,13 @@ function hyperfixi(options = {}) {
3533
3889
  var index_default = hyperfixi;
3534
3890
  // Annotate the CommonJS export names for ESM import in node:
3535
3891
  0 && (module.exports = {
3892
+ DomainAggregator,
3893
+ DomainScanner,
3536
3894
  REGIONS,
3537
3895
  SUPPORTED_LANGUAGES,
3538
3896
  clearCustomKeywords,
3539
3897
  clearSemanticParser,
3898
+ descriptorToScanRule,
3540
3899
  detectLanguages,
3541
3900
  getAllLanguageCodes,
3542
3901
  getKeywordsForLanguage,