lingui-npm-plugin.spec.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { readFile, rm } from 'node:fs/promises';
  2. import { join } from 'node:path';
  3. import tsconfigPaths from 'tsconfig-paths';
  4. import { describe, expect, it } from 'vitest';
  5. import { compile } from '../utils/compiler.js';
  6. import { debugLogger, noopLogger } from '../utils/logger.js';
  7. import { linguiBabelPlugin } from '../vite-plugin-lingui-babel.js';
  8. /**
  9. * Integration tests for the linguiBabelPlugin with actual npm packages.
  10. *
  11. * These tests verify that:
  12. * 1. The plugin discovery mechanism correctly identifies npm packages with dashboard extensions
  13. * 2. The linguiBabelPlugin transforms Lingui macros in those discovered packages
  14. *
  15. * This addresses the bug where third-party npm packages (like @vendure-ee/*) with
  16. * dashboard extensions using Lingui macros would fail to build because the macros
  17. * weren't being transformed.
  18. *
  19. * @see LINGUI_BABEL_PLUGIN_BUG.md
  20. */
  21. describe('linguiBabelPlugin with npm packages containing Lingui macros', () => {
  22. const fixtureDir = join(__dirname, 'fixtures-lingui-npm-plugin');
  23. const fakeNodeModules = join(fixtureDir, 'fake_node_modules');
  24. const tempDir = join(__dirname, './__temp/lingui-npm-plugin');
  25. it('should discover npm plugin and transform its Lingui macros', { timeout: 60_000 }, async () => {
  26. // Clean up temp directory
  27. await rm(tempDir, { recursive: true, force: true });
  28. // Register tsconfig paths so the test can resolve the fake npm package
  29. tsconfigPaths.register({
  30. baseUrl: fakeNodeModules,
  31. paths: {
  32. '@test-scope/lingui-plugin': [join(fakeNodeModules, '@test-scope/lingui-plugin')],
  33. },
  34. });
  35. // Step 1: Compile and discover plugins (like the real build process does)
  36. const result = await compile({
  37. outputPath: tempDir,
  38. vendureConfigPath: join(fixtureDir, 'vendure-config.ts'),
  39. logger: process.env.LOG ? debugLogger : noopLogger,
  40. pluginPackageScanner: {
  41. nodeModulesRoot: fakeNodeModules,
  42. },
  43. });
  44. // Verify the plugin was discovered
  45. expect(result.pluginInfo).toHaveLength(1);
  46. expect(result.pluginInfo[0].name).toBe('LinguiTestPlugin');
  47. expect(result.pluginInfo[0].dashboardEntryPath).toBe('./dashboard/index.tsx');
  48. expect(result.pluginInfo[0].sourcePluginPath).toBeUndefined(); // npm package, not local
  49. // Step 2: Extract the package path from the discovered plugin
  50. // (This is what the linguiBabelPlugin does in configResolved)
  51. const pluginPath = result.pluginInfo[0].pluginPath;
  52. expect(pluginPath).toContain('@test-scope/lingui-plugin');
  53. // Extract package name from path (simulating extractPackagePath)
  54. const packageName = '@test-scope/lingui-plugin';
  55. // Step 3: Create linguiBabelPlugin with the discovered package
  56. const plugin = linguiBabelPlugin({
  57. additionalPackagePaths: [packageName],
  58. });
  59. // Step 4: Read the actual dashboard file that contains Lingui macros
  60. const dashboardFilePath = join(fakeNodeModules, '@test-scope/lingui-plugin/dashboard/index.tsx');
  61. const dashboardCode = await readFile(dashboardFilePath, 'utf-8');
  62. // Verify the file contains Lingui macros before transformation
  63. expect(dashboardCode).toContain("from '@lingui/react/macro'");
  64. expect(dashboardCode).toContain("from '@lingui/core/macro'");
  65. expect(dashboardCode).toContain('<Trans>');
  66. expect(dashboardCode).toContain('t`');
  67. // Step 5: Transform the file using linguiBabelPlugin
  68. // @ts-expect-error - transform expects a different context but works for testing
  69. const transformed = await plugin.transform(dashboardCode, dashboardFilePath);
  70. // Step 6: Verify the macros were transformed
  71. expect(transformed).not.toBeNull();
  72. expect(transformed?.code).toBeDefined();
  73. // The transformed code should NOT contain macro imports
  74. expect(transformed?.code).not.toContain("from '@lingui/react/macro'");
  75. expect(transformed?.code).not.toContain("from '@lingui/core/macro'");
  76. // The transformed code should contain the runtime imports instead
  77. expect(transformed?.code).toContain('@lingui/react');
  78. expect(transformed?.code).toContain('@lingui/core');
  79. });
  80. it('should NOT transform Lingui macros in undiscovered npm packages', { timeout: 60_000 }, async () => {
  81. // Create plugin WITHOUT the package in additionalPackagePaths
  82. // (simulating a package that wasn't discovered as a Vendure plugin)
  83. const plugin = linguiBabelPlugin({
  84. additionalPackagePaths: [], // Empty - no packages discovered
  85. });
  86. // Read the dashboard file
  87. const dashboardFilePath = join(fakeNodeModules, '@test-scope/lingui-plugin/dashboard/index.tsx');
  88. const dashboardCode = await readFile(dashboardFilePath, 'utf-8');
  89. // Try to transform - should return null because package isn't in allowlist
  90. // @ts-expect-error - transform expects a different context but works for testing
  91. const transformed = await plugin.transform(dashboardCode, dashboardFilePath);
  92. // Should be null - file was skipped because it's in node_modules and not discovered
  93. expect(transformed).toBeNull();
  94. });
  95. it('should still transform @vendure/dashboard source files without discovery', async () => {
  96. // Create plugin with no discovered packages
  97. const plugin = linguiBabelPlugin();
  98. const code = `
  99. import { Trans } from '@lingui/react/macro';
  100. export function MyComponent() {
  101. return <Trans>Hello</Trans>;
  102. }
  103. `;
  104. // Simulate a file from @vendure/dashboard/src
  105. const id = join(fakeNodeModules, '@vendure/dashboard/src/components/Test.tsx');
  106. // @ts-expect-error - transform expects a different context but works for testing
  107. const transformed = await plugin.transform(code, id);
  108. // Should transform because @vendure/dashboard/src is always allowed
  109. expect(transformed).not.toBeNull();
  110. expect(transformed?.code).not.toContain("from '@lingui/react/macro'");
  111. });
  112. it('should still transform local files without discovery', async () => {
  113. // Create plugin with no discovered packages
  114. const plugin = linguiBabelPlugin();
  115. const code = `
  116. import { Trans } from '@lingui/react/macro';
  117. export function MyComponent() {
  118. return <Trans>Hello</Trans>;
  119. }
  120. `;
  121. // Simulate a local file (not in node_modules)
  122. const id = '/path/to/project/src/plugins/my-plugin/dashboard/Test.tsx';
  123. // @ts-expect-error - transform expects a different context but works for testing
  124. const transformed = await plugin.transform(code, id);
  125. // Should transform because local files are always allowed
  126. expect(transformed).not.toBeNull();
  127. expect(transformed?.code).not.toContain("from '@lingui/react/macro'");
  128. });
  129. });