@hyperfixi/vite-plugin 2.0.0 → 2.2.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.js CHANGED
@@ -473,6 +473,8 @@ var POLISH_KEYWORDS = /* @__PURE__ */ new Set([
473
473
  "zwi\u0119ksz",
474
474
  "zwieksz",
475
475
  "zmniejsz",
476
+ "uzyskaj",
477
+ "pobierz",
476
478
  "wywo\u0142aj",
477
479
  "wywolaj",
478
480
  "wyzw\xF3l",
@@ -1823,6 +1825,15 @@ async function evaluate(node, ctx) {
1823
1825
  const obj = await evaluate(node.object, ctx);
1824
1826
  if (obj == null) return undefined;
1825
1827
  const prop = node.computed ? await evaluate(node.property, ctx) : node.property;
1828
+ if (prop === 'values' && obj instanceof Element) {
1829
+ if (obj instanceof HTMLFormElement) return new FormData(obj);
1830
+ const fd = new FormData();
1831
+ obj.querySelectorAll('input, select, textarea').forEach(input => {
1832
+ const name = input.getAttribute('name');
1833
+ if (name && 'value' in input) fd.append(name, input.value);
1834
+ });
1835
+ return fd;
1836
+ }
1826
1837
  return obj[prop];
1827
1838
 
1828
1839
  case 'call': {
@@ -2907,6 +2918,9 @@ function compileExpression(node) {
2907
2918
  if (prop.startsWith("*")) {
2908
2919
  return `${obj}.style.${prop.slice(1)}`;
2909
2920
  }
2921
+ if (prop === "values") {
2922
+ 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})`;
2923
+ }
2910
2924
  return `${obj}.${prop}`;
2911
2925
  }
2912
2926
  case "member": {
@@ -3230,6 +3244,290 @@ function extractScripts(html) {
3230
3244
  return scripts;
3231
3245
  }
3232
3246
 
3247
+ // src/server-bridge-integration.ts
3248
+ import { resolve, join } from "path";
3249
+ import { mkdir, writeFile } from "fs/promises";
3250
+ var serverBridge = null;
3251
+ var loadAttempted = false;
3252
+ var lastRouteHash = "";
3253
+ async function loadServerBridge(debug) {
3254
+ if (loadAttempted) return serverBridge;
3255
+ loadAttempted = true;
3256
+ try {
3257
+ const mod = await import("@hyperfixi/server-bridge");
3258
+ serverBridge = mod;
3259
+ if (debug) {
3260
+ console.log("[hyperfixi] Server-bridge loaded successfully");
3261
+ }
3262
+ return serverBridge;
3263
+ } catch {
3264
+ console.warn(
3265
+ "[hyperfixi] @hyperfixi/server-bridge is not installed. Install it to enable server route generation: npm install @hyperfixi/server-bridge"
3266
+ );
3267
+ return null;
3268
+ }
3269
+ }
3270
+ function computeRouteHash(routes) {
3271
+ return routes.map((r) => `${r.method}:${r.path}`).sort().join("|");
3272
+ }
3273
+ async function runServerBridge(cwd, options, debug, force) {
3274
+ const mod = serverBridge ?? await loadServerBridge(debug);
3275
+ if (!mod) return;
3276
+ try {
3277
+ const outputDir = resolve(cwd, options.output ?? "./server/routes");
3278
+ const result = await mod.generate({
3279
+ dir: cwd,
3280
+ framework: options.framework ?? "express",
3281
+ output: options.output,
3282
+ typescript: options.typescript,
3283
+ ignore: options.ignore
3284
+ });
3285
+ const routeHash = computeRouteHash(result.routes);
3286
+ if (!force && routeHash === lastRouteHash) {
3287
+ if (debug) {
3288
+ console.log("[hyperfixi] Server-bridge: routes unchanged, skipping generation");
3289
+ }
3290
+ return;
3291
+ }
3292
+ lastRouteHash = routeHash;
3293
+ if (result.routes.length === 0) {
3294
+ if (debug) {
3295
+ console.log("[hyperfixi] Server-bridge: no routes found");
3296
+ }
3297
+ return;
3298
+ }
3299
+ await mkdir(outputDir, { recursive: true });
3300
+ for (const file of result.generated.files) {
3301
+ const filePath = join(outputDir, file.path);
3302
+ const fileDir = join(filePath, "..");
3303
+ await mkdir(fileDir, { recursive: true });
3304
+ await writeFile(filePath, file.content, "utf-8");
3305
+ }
3306
+ const manifest = mod.createManifest(result.routes);
3307
+ await mod.saveManifest(outputDir, manifest);
3308
+ if (debug) {
3309
+ console.log(
3310
+ `[hyperfixi] Server-bridge: generated ${result.generated.files.length} file(s) for ${result.routes.length} route(s) in ${outputDir}/`
3311
+ );
3312
+ }
3313
+ if (result.scan.conflicts.length > 0) {
3314
+ console.warn(
3315
+ `[hyperfixi] Server-bridge: ${result.scan.conflicts.length} route conflict(s) detected`
3316
+ );
3317
+ }
3318
+ } catch (err) {
3319
+ console.error(
3320
+ "[hyperfixi] Server-bridge generation failed:",
3321
+ err instanceof Error ? err.message : err
3322
+ );
3323
+ }
3324
+ }
3325
+
3326
+ // src/domain-scanner.ts
3327
+ function buildAttributePatterns(attributes) {
3328
+ const patterns = [];
3329
+ for (const attr of attributes) {
3330
+ const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3331
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*"([^"]+)"`, "g"));
3332
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*'([^']+)'`, "g"));
3333
+ patterns.push(new RegExp(`${escaped}\\s*=\\s*\`([^\`]+)\``, "g"));
3334
+ }
3335
+ return patterns;
3336
+ }
3337
+ function buildScriptPatterns(scriptTypes) {
3338
+ return scriptTypes.map((type) => {
3339
+ const escaped = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3340
+ return new RegExp(`<script[^>]*type=["']?${escaped}["']?[^>]*>([\\s\\S]*?)<\\/script>`, "gi");
3341
+ });
3342
+ }
3343
+ function detectKeywords(snippet, keywords) {
3344
+ const detectedKeywords = /* @__PURE__ */ new Set();
3345
+ const detectedLanguages = /* @__PURE__ */ new Set();
3346
+ for (const [lang, kwList] of Object.entries(keywords)) {
3347
+ for (const kw of kwList) {
3348
+ const isLatin = /^[a-zA-Z]/.test(kw);
3349
+ const found = isLatin ? new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(snippet) : snippet.includes(kw);
3350
+ if (found) {
3351
+ detectedKeywords.add(kw);
3352
+ detectedLanguages.add(lang);
3353
+ }
3354
+ }
3355
+ }
3356
+ return { detectedKeywords, detectedLanguages };
3357
+ }
3358
+ var DomainScanner = class {
3359
+ constructor(rules) {
3360
+ this.rules = rules;
3361
+ const allAttrs = rules.flatMap((r) => [...r.attributes]);
3362
+ const allScriptTypes = rules.flatMap((r) => r.scriptTypes ? [...r.scriptTypes] : []);
3363
+ const parts = [];
3364
+ for (const attr of allAttrs) {
3365
+ parts.push(attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3366
+ }
3367
+ for (const type of allScriptTypes) {
3368
+ parts.push(type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3369
+ }
3370
+ this.quickCheckPattern = parts.length > 0 ? new RegExp(parts.join("|"), "i") : null;
3371
+ }
3372
+ /**
3373
+ * Fast pre-check: does this code contain any domain-related attributes?
3374
+ */
3375
+ hasAnyDomainUsage(code) {
3376
+ if (!this.quickCheckPattern) return false;
3377
+ return this.quickCheckPattern.test(code);
3378
+ }
3379
+ /**
3380
+ * Scan a file for all registered domain syntaxes.
3381
+ * Returns one DomainFileUsage per domain that had matches.
3382
+ */
3383
+ scan(code, _id) {
3384
+ const results = [];
3385
+ for (const rule of this.rules) {
3386
+ const snippets = [];
3387
+ const attrPatterns = buildAttributePatterns(rule.attributes);
3388
+ for (const pattern of attrPatterns) {
3389
+ pattern.lastIndex = 0;
3390
+ let match;
3391
+ while (match = pattern.exec(code)) {
3392
+ const value = match[1].trim();
3393
+ if (value) snippets.push(value);
3394
+ }
3395
+ }
3396
+ if (rule.scriptTypes) {
3397
+ const scriptPatterns = buildScriptPatterns(rule.scriptTypes);
3398
+ for (const pattern of scriptPatterns) {
3399
+ pattern.lastIndex = 0;
3400
+ let match;
3401
+ while (match = pattern.exec(code)) {
3402
+ const value = match[1].trim();
3403
+ if (value) snippets.push(value);
3404
+ }
3405
+ }
3406
+ }
3407
+ if (snippets.length === 0) continue;
3408
+ const allDetectedKeywords = /* @__PURE__ */ new Set();
3409
+ const allDetectedLanguages = /* @__PURE__ */ new Set();
3410
+ if (rule.keywords) {
3411
+ for (const snippet of snippets) {
3412
+ const { detectedKeywords, detectedLanguages } = detectKeywords(snippet, rule.keywords);
3413
+ for (const kw of detectedKeywords) allDetectedKeywords.add(kw);
3414
+ for (const lang of detectedLanguages) allDetectedLanguages.add(lang);
3415
+ }
3416
+ }
3417
+ results.push({
3418
+ domain: rule.domain,
3419
+ detectedKeywords: allDetectedKeywords,
3420
+ detectedLanguages: allDetectedLanguages,
3421
+ snippetCount: snippets.length
3422
+ });
3423
+ }
3424
+ return results;
3425
+ }
3426
+ };
3427
+ function descriptorToScanRule(descriptor) {
3428
+ if (!descriptor.scanConfig) return null;
3429
+ return {
3430
+ domain: descriptor.name,
3431
+ attributes: [...descriptor.scanConfig.attributes],
3432
+ scriptTypes: descriptor.scanConfig.scriptTypes ? [...descriptor.scanConfig.scriptTypes] : void 0,
3433
+ keywords: descriptor.scanConfig.keywords ? Object.fromEntries(
3434
+ Object.entries(descriptor.scanConfig.keywords).map(([k, v]) => [k, [...v]])
3435
+ ) : void 0
3436
+ };
3437
+ }
3438
+
3439
+ // src/domain-aggregator.ts
3440
+ var DomainAggregator = class {
3441
+ constructor() {
3442
+ this.fileUsage = /* @__PURE__ */ new Map();
3443
+ this.cache = null;
3444
+ }
3445
+ /**
3446
+ * Add or update domain usage for a file.
3447
+ * Returns true if usage changed (invalidates cache).
3448
+ */
3449
+ add(filePath, usage) {
3450
+ const existing = this.fileUsage.get(filePath);
3451
+ const changed = !existing || !this.isEqual(existing, usage);
3452
+ if (changed) {
3453
+ this.fileUsage.set(filePath, usage);
3454
+ this.cache = null;
3455
+ }
3456
+ return changed;
3457
+ }
3458
+ /**
3459
+ * Remove a file's domain usage.
3460
+ * Returns true if the file was tracked.
3461
+ */
3462
+ remove(filePath) {
3463
+ const had = this.fileUsage.has(filePath);
3464
+ if (had) {
3465
+ this.fileUsage.delete(filePath);
3466
+ this.cache = null;
3467
+ }
3468
+ return had;
3469
+ }
3470
+ /**
3471
+ * Get aggregated usage across all files.
3472
+ * Result is cached until add/remove changes it.
3473
+ */
3474
+ getUsage() {
3475
+ if (this.cache) return this.cache;
3476
+ const domainMap = /* @__PURE__ */ new Map();
3477
+ for (const usages of this.fileUsage.values()) {
3478
+ for (const usage of usages) {
3479
+ let entry = domainMap.get(usage.domain);
3480
+ if (!entry) {
3481
+ entry = {
3482
+ detectedKeywords: /* @__PURE__ */ new Set(),
3483
+ detectedLanguages: /* @__PURE__ */ new Set(),
3484
+ fileCount: 0,
3485
+ totalSnippets: 0
3486
+ };
3487
+ domainMap.set(usage.domain, entry);
3488
+ }
3489
+ entry.fileCount++;
3490
+ entry.totalSnippets += usage.snippetCount;
3491
+ for (const kw of usage.detectedKeywords) entry.detectedKeywords.add(kw);
3492
+ for (const lang of usage.detectedLanguages) entry.detectedLanguages.add(lang);
3493
+ }
3494
+ }
3495
+ this.cache = Array.from(domainMap.entries()).map(([domain, data]) => ({
3496
+ domain,
3497
+ detectedKeywords: data.detectedKeywords,
3498
+ detectedLanguages: data.detectedLanguages,
3499
+ fileCount: data.fileCount,
3500
+ totalSnippets: data.totalSnippets
3501
+ }));
3502
+ return this.cache;
3503
+ }
3504
+ /**
3505
+ * Get aggregated usage for a specific domain.
3506
+ */
3507
+ getUsageForDomain(domain) {
3508
+ return this.getUsage().find((u) => u.domain === domain) ?? null;
3509
+ }
3510
+ /**
3511
+ * Clear all tracked data.
3512
+ */
3513
+ clear() {
3514
+ this.fileUsage.clear();
3515
+ this.cache = null;
3516
+ }
3517
+ // ---------------------------------------------------------------------------
3518
+ // Private
3519
+ // ---------------------------------------------------------------------------
3520
+ isEqual(a, b) {
3521
+ if (a.length !== b.length) return false;
3522
+ for (let i = 0; i < a.length; i++) {
3523
+ if (a[i].domain !== b[i].domain) return false;
3524
+ if (a[i].snippetCount !== b[i].snippetCount) return false;
3525
+ if (a[i].detectedKeywords.size !== b[i].detectedKeywords.size) return false;
3526
+ }
3527
+ return true;
3528
+ }
3529
+ };
3530
+
3233
3531
  // src/index.ts
3234
3532
  var VIRTUAL_MODULE_ID = "virtual:hyperfixi";
3235
3533
  var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
@@ -3239,6 +3537,8 @@ function hyperfixi(options = {}) {
3239
3537
  const scanner = new Scanner(options);
3240
3538
  const aggregator = new Aggregator();
3241
3539
  const generator = new Generator(options);
3540
+ const domainScanner = options.domains && options.domains.length > 0 ? new DomainScanner(options.domains) : null;
3541
+ const domainAggregator = domainScanner ? new DomainAggregator() : null;
3242
3542
  let server = null;
3243
3543
  let cachedBundle = null;
3244
3544
  let lastUsageHash = "";
@@ -3341,11 +3641,14 @@ function hyperfixi(options = {}) {
3341
3641
  /**
3342
3642
  * Configure plugin based on Vite mode
3343
3643
  */
3344
- configResolved(config) {
3644
+ async configResolved(config) {
3345
3645
  isDev = config.command === "serve";
3346
3646
  if (options.debug) {
3347
3647
  console.log("[hyperfixi] Plugin initialized, mode:", isDev ? "development" : "production");
3348
3648
  }
3649
+ if (options.serverBridge) {
3650
+ await loadServerBridge(options.debug);
3651
+ }
3349
3652
  },
3350
3653
  /**
3351
3654
  * Store server reference and pre-scan HTML files for HMR
@@ -3358,6 +3661,25 @@ function hyperfixi(options = {}) {
3358
3661
  }
3359
3662
  const scannedFiles = await scanner.scanProject(cwd);
3360
3663
  aggregator.loadFromScan(scannedFiles);
3664
+ if (domainScanner && domainAggregator) {
3665
+ for (const [filePath] of scannedFiles) {
3666
+ try {
3667
+ const fs = await import("fs");
3668
+ const content = fs.readFileSync(filePath, "utf-8");
3669
+ if (domainScanner.hasAnyDomainUsage(content)) {
3670
+ const domainUsages = domainScanner.scan(content, filePath);
3671
+ if (domainUsages.length > 0) {
3672
+ domainAggregator.add(filePath, domainUsages);
3673
+ }
3674
+ }
3675
+ } catch {
3676
+ }
3677
+ }
3678
+ if (options.debug) {
3679
+ const domainSummary = domainAggregator.getUsage();
3680
+ console.log("[hyperfixi] Domain pre-scan:", domainSummary);
3681
+ }
3682
+ }
3361
3683
  if (options.debug) {
3362
3684
  const summary = aggregator.getSummary();
3363
3685
  console.log("[hyperfixi] Pre-scan complete:", summary);
@@ -3404,6 +3726,12 @@ function hyperfixi(options = {}) {
3404
3726
  }
3405
3727
  const usage = scanner.scan(code, id);
3406
3728
  const changed = aggregator.add(id, usage);
3729
+ if (domainScanner && domainAggregator && domainScanner.hasAnyDomainUsage(code)) {
3730
+ const domainUsages = domainScanner.scan(code, id);
3731
+ if (domainUsages.length > 0) {
3732
+ domainAggregator.add(id, domainUsages);
3733
+ }
3734
+ }
3407
3735
  if (changed && server) {
3408
3736
  setImmediate(() => {
3409
3737
  const currentHash = computeUsageHash(aggregator.getUsage());
@@ -3433,6 +3761,9 @@ function hyperfixi(options = {}) {
3433
3761
  invalidateVirtualModule();
3434
3762
  }
3435
3763
  }
3764
+ if (options.serverBridge && server) {
3765
+ await runServerBridge(server.config.root, options.serverBridge, options.debug);
3766
+ }
3436
3767
  } catch {
3437
3768
  if (aggregator.remove(file)) {
3438
3769
  invalidateVirtualModule();
@@ -3463,6 +3794,25 @@ function hyperfixi(options = {}) {
3463
3794
  }
3464
3795
  const scannedFiles = await scanner.scanProject(cwd);
3465
3796
  aggregator.loadFromScan(scannedFiles);
3797
+ if (domainScanner && domainAggregator) {
3798
+ for (const [filePath] of scannedFiles) {
3799
+ try {
3800
+ const fs = await import("fs");
3801
+ const content = fs.readFileSync(filePath, "utf-8");
3802
+ if (domainScanner.hasAnyDomainUsage(content)) {
3803
+ const domainUsages = domainScanner.scan(content, filePath);
3804
+ if (domainUsages.length > 0) {
3805
+ domainAggregator.add(filePath, domainUsages);
3806
+ }
3807
+ }
3808
+ } catch {
3809
+ }
3810
+ }
3811
+ if (options.debug) {
3812
+ const domainSummary = domainAggregator.getUsage();
3813
+ console.log("[hyperfixi] Domain build scan:", domainSummary);
3814
+ }
3815
+ }
3466
3816
  if (mode === "compile") {
3467
3817
  const usage = aggregator.getUsage();
3468
3818
  if (usage.detectedLanguages.size > 0) {
@@ -3488,17 +3838,23 @@ function hyperfixi(options = {}) {
3488
3838
  languages: summary.languages
3489
3839
  });
3490
3840
  }
3841
+ if (options.serverBridge) {
3842
+ await runServerBridge(cwd, options.serverBridge, options.debug, true);
3843
+ }
3491
3844
  }
3492
3845
  }
3493
3846
  };
3494
3847
  }
3495
3848
  var index_default = hyperfixi;
3496
3849
  export {
3850
+ DomainAggregator,
3851
+ DomainScanner,
3497
3852
  REGIONS,
3498
3853
  SUPPORTED_LANGUAGES,
3499
3854
  clearCustomKeywords,
3500
3855
  clearSemanticParser,
3501
3856
  index_default as default,
3857
+ descriptorToScanRule,
3502
3858
  detectLanguages,
3503
3859
  getAllLanguageCodes,
3504
3860
  getKeywordsForLanguage,