@usefragments/core 2.0.1 → 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.
@@ -344,3 +344,249 @@ describe("components/prefer-library — #9e directory import-path identity", ()
344
344
  expect(ruleComponentsPreferLibrary(ix)).toHaveLength(1);
345
345
  });
346
346
  });
347
+
348
+ describe("source-qualified canonical mapping execution", () => {
349
+ const mappings = ["first", "second"].map((name) => ({
350
+ name: "Button",
351
+ canonical: "Button",
352
+ htmlEquivalent: "button",
353
+ importPath: `@acme/${name}`,
354
+ source: { kind: "package", importPath: `@acme/${name}`, exportName: "Button" },
355
+ propMapping: [{ rawProp: "title", canonicalProp: name === "first" ? "label" : "caption" }],
356
+ }));
357
+ function scan(input: { reverse?: boolean; sourceQualified?: boolean; imported?: string }) {
358
+ const ix = new FactIndex();
359
+ ix.addMany(
360
+ compileGlobalGovernanceFacts({
361
+ rules: {
362
+ "components/prefer-library": {
363
+ enabled: true,
364
+ severity: "error",
365
+ options: {
366
+ ...(input.sourceQualified ? { sourceIdentitySchema: "component-source:v1" } : {}),
367
+ canonicalMappings: input.reverse ? [...mappings].reverse() : mappings,
368
+ },
369
+ },
370
+ },
371
+ })
372
+ );
373
+ const element = input.imported ? "Button" : "button";
374
+ ix.add(
375
+ makeUsageNodeFact({
376
+ file: "src/App.tsx",
377
+ nodePath: "0",
378
+ element,
379
+ location: { file: "src/App.tsx", line: 1, column: 1 },
380
+ })
381
+ );
382
+ if (input.imported)
383
+ ix.add(
384
+ makeUsageImportFact({
385
+ file: "src/App.tsx",
386
+ local: "Button",
387
+ imported: "Button",
388
+ source: input.imported,
389
+ location: { file: "src/App.tsx", line: 1, column: 1 },
390
+ })
391
+ );
392
+ return ruleComponentsPreferLibrary(ix);
393
+ }
394
+ it("retains legacy first-target guidance for unmarked policy", () => {
395
+ expect(scan({})[0]?.attributes?.suggestedImport).toBe("@acme/first");
396
+ });
397
+ it("does not arbitrarily select props or an import when multiple approved targets fit", () => {
398
+ const forward = scan({ sourceQualified: true });
399
+ const withoutCapturedPolicy = (findings: ReturnType<typeof scan>) =>
400
+ findings.map(({ evidence: _evidence, ...finding }) => finding);
401
+ expect(withoutCapturedPolicy(forward)).toEqual(
402
+ withoutCapturedPolicy(scan({ sourceQualified: true, reverse: true }))
403
+ );
404
+ expect(forward).toHaveLength(1);
405
+ expect(forward[0]?.attributes?.ambiguousCanonicalTarget).toBe(true);
406
+ expect(forward[0]?.fix).toBeUndefined();
407
+ expect(forward[0]?.attributes).not.toHaveProperty("propMapping");
408
+ });
409
+ it.each(["@acme/first", "@acme/second"])(
410
+ "recognizes the exact approved import %s in either catalog order",
411
+ (imported) => {
412
+ expect(scan({ sourceQualified: true, imported })).toEqual([]);
413
+ expect(scan({ sourceQualified: true, imported, reverse: true })).toEqual([]);
414
+ }
415
+ );
416
+ function barrelScan(source: string, imported: string) {
417
+ const ix = new FactIndex();
418
+ ix.addMany(
419
+ compileGlobalGovernanceFacts({
420
+ rules: {
421
+ "components/prefer-library": {
422
+ enabled: true,
423
+ severity: "error",
424
+ options: {
425
+ sourceIdentitySchema: "component-source:v1",
426
+ canonicalMappings: ["first", "second"].map((folder) => ({
427
+ name: "Button",
428
+ canonical: "Button",
429
+ htmlEquivalent: "button",
430
+ importPath: `@acme/${folder}`,
431
+ source: { kind: "repository", path: `${folder}/Button.tsx`, symbol: "Button" },
432
+ exportAddresses: [
433
+ { kind: "package", importPath: "@acme/public", exportName: `${folder}Button` },
434
+ ],
435
+ })),
436
+ },
437
+ },
438
+ },
439
+ })
440
+ );
441
+ ix.add(
442
+ makeUsageNodeFact({
443
+ file: "src/App.tsx",
444
+ nodePath: "0",
445
+ element: "Button",
446
+ location: { file: "src/App.tsx", line: 1, column: 1 },
447
+ })
448
+ );
449
+ ix.add(
450
+ makeUsageImportFact({
451
+ file: "src/App.tsx",
452
+ local: "Button",
453
+ imported,
454
+ source,
455
+ location: { file: "src/App.tsx", line: 1, column: 1 },
456
+ })
457
+ );
458
+ return ruleComponentsPreferLibrary(ix);
459
+ }
460
+ it("recognizes a proven barrel export independently of its implementation symbol", () => {
461
+ expect(barrelScan("@acme/public", "secondButton")).toEqual([]);
462
+ });
463
+ it("does not authorize the definition symbol at an unproven package address", () => {
464
+ expect(barrelScan("@acme/second", "Button")[0]?.attributes?.ambiguousCanonicalTarget).toBe(
465
+ true
466
+ );
467
+ });
468
+ it("suggests the actual proven public export name while preserving the component identity", () => {
469
+ const ix = new FactIndex();
470
+ ix.addMany(
471
+ compileGlobalGovernanceFacts({
472
+ rules: {
473
+ "components/prefer-library": {
474
+ enabled: true,
475
+ severity: "error",
476
+ options: {
477
+ sourceIdentitySchema: "component-source:v1",
478
+ canonicalMappings: [
479
+ {
480
+ name: "Button",
481
+ canonical: "Button",
482
+ htmlEquivalent: "button",
483
+ importPath: "@acme/unproven",
484
+ source: { kind: "repository", path: "src/Button.tsx", symbol: "Button" },
485
+ exportAddresses: [
486
+ { kind: "package", importPath: "@acme/public", exportName: "PublicButton" },
487
+ ],
488
+ },
489
+ ],
490
+ },
491
+ },
492
+ },
493
+ })
494
+ );
495
+ ix.add(
496
+ makeUsageNodeFact({
497
+ file: "src/App.tsx",
498
+ nodePath: "0",
499
+ element: "button",
500
+ location: { file: "src/App.tsx", line: 1, column: 1 },
501
+ })
502
+ );
503
+ expect(ruleComponentsPreferLibrary(ix)[0]?.attributes).toMatchObject({
504
+ suggestedComponent: "PublicButton",
505
+ suggestedImport: "@acme/public",
506
+ });
507
+ });
508
+ it("does not fabricate a JSX replacement when one source has several public aliases", () => {
509
+ const ix = new FactIndex();
510
+ ix.addMany(
511
+ compileGlobalGovernanceFacts({
512
+ rules: {
513
+ "components/prefer-library": {
514
+ enabled: true,
515
+ severity: "error",
516
+ options: {
517
+ sourceIdentitySchema: "component-source:v1",
518
+ canonicalMappings: [
519
+ {
520
+ name: "Button",
521
+ canonical: "Button",
522
+ htmlEquivalent: "button",
523
+ source: { kind: "repository", path: "src/Button.tsx", symbol: "Button" },
524
+ exportAddresses: ["PrimaryButton", "SecondaryButton"].map((exportName) => ({
525
+ kind: "package",
526
+ importPath: "@acme/public",
527
+ exportName,
528
+ })),
529
+ },
530
+ ],
531
+ },
532
+ },
533
+ },
534
+ })
535
+ );
536
+ ix.add(
537
+ makeUsageNodeFact({
538
+ file: "src/App.tsx",
539
+ nodePath: "0",
540
+ element: "button",
541
+ location: { file: "src/App.tsx", line: 1, column: 1 },
542
+ })
543
+ );
544
+ const finding = ruleComponentsPreferLibrary(ix)[0];
545
+ expect(finding?.attributes?.unresolvedCanonicalExport).toBe(true);
546
+ expect(finding?.fix).toBeUndefined();
547
+ expect(finding?.attributes).not.toHaveProperty("suggestedComponent");
548
+ expect(finding?.attributes).not.toHaveProperty("propMapping");
549
+ });
550
+ it("keeps role-only unresolved export guidance advisory", () => {
551
+ const ix = new FactIndex();
552
+ ix.addMany(
553
+ compileGlobalGovernanceFacts({
554
+ rules: {
555
+ "components/prefer-library": {
556
+ enabled: true,
557
+ severity: "error",
558
+ options: {
559
+ sourceIdentitySchema: "component-source:v1",
560
+ canonicalMappings: [
561
+ {
562
+ name: "Toast",
563
+ canonical: "Toast",
564
+ ariaRole: "status",
565
+ source: { kind: "repository", path: "src/Toast.tsx", symbol: "Toast" },
566
+ exportAddresses: [],
567
+ },
568
+ ],
569
+ },
570
+ },
571
+ },
572
+ })
573
+ );
574
+ ix.add(
575
+ makeUsageNodeFact({
576
+ file: "src/App.tsx",
577
+ nodePath: "0",
578
+ element: "div",
579
+ role: "status",
580
+ location: { file: "src/App.tsx", line: 1, column: 1 },
581
+ })
582
+ );
583
+ const finding = ruleComponentsPreferLibrary(ix)[0];
584
+ expect(finding?.severity).toBe("moderate");
585
+ expect(finding?.attributes).toMatchObject({
586
+ unresolvedCanonicalExport: true,
587
+ advisory: true,
588
+ precisionTier: "role-reimpl",
589
+ });
590
+ expect(finding?.fix).toBeUndefined();
591
+ });
592
+ });