check-lib-imports.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. #!/usr/bin/env node
  2. import fs from 'fs';
  3. import path from 'path';
  4. import process from 'process';
  5. import { fileURLToPath } from 'url';
  6. const __filename = fileURLToPath(import.meta.url);
  7. const __dirname = path.dirname(__filename);
  8. // Normalize paths to ensure cross-platform compatibility
  9. const normalizePath = p => path.normalize(p);
  10. // Check if we're running from the dashboard directory or root directory
  11. const currentDir = normalizePath(process.cwd());
  12. const isDashboardDir = currentDir.endsWith(normalizePath('packages/dashboard'));
  13. const HOOKS_DIR = isDashboardDir
  14. ? normalizePath(path.join(__dirname, '../src/lib/hooks'))
  15. : normalizePath(path.join(currentDir, 'packages/dashboard/src/lib/hooks'));
  16. const DASHBOARD_SRC_DIR = isDashboardDir
  17. ? normalizePath(path.join(__dirname, '../src'))
  18. : normalizePath(path.join(currentDir, 'packages/dashboard/src'));
  19. // Required prefix for imports in hook files
  20. const REQUIRED_PREFIX = '@/vdb';
  21. // Banned import pattern
  22. const BANNED_IMPORT = '@/vdb/index.js';
  23. // Lib directory (auto-exported via index.ts)
  24. const LIB_DIR = isDashboardDir
  25. ? normalizePath(path.join(__dirname, '../src/lib'))
  26. : normalizePath(path.join(currentDir, 'packages/dashboard/src/lib'));
  27. // Files allowed to have createContext + useContext in the same file.
  28. // These are UI primitives (e.g., shadcn components) where the context is internal
  29. // and not intended to be accessed by extensions.
  30. const CONTEXT_PATTERN_ALLOWLIST = [
  31. 'components/ui/carousel.tsx',
  32. 'components/ui/chart.tsx',
  33. 'components/ui/form.tsx',
  34. 'components/ui/toggle-group.tsx',
  35. ];
  36. function findHookFiles(dir) {
  37. const files = [];
  38. const items = fs.readdirSync(dir);
  39. for (const item of items) {
  40. const fullPath = normalizePath(path.join(dir, item));
  41. const stat = fs.statSync(fullPath);
  42. if (stat.isFile() && item.startsWith('use-') && (item.endsWith('.ts') || item.endsWith('.tsx'))) {
  43. files.push(fullPath);
  44. }
  45. }
  46. return files;
  47. }
  48. function findDashboardFiles(dir) {
  49. const files = [];
  50. function scanDirectory(currentDir) {
  51. const items = fs.readdirSync(currentDir);
  52. for (const item of items) {
  53. const fullPath = normalizePath(path.join(currentDir, item));
  54. const stat = fs.statSync(fullPath);
  55. if (stat.isDirectory()) {
  56. if (!['node_modules', '.git', 'dist', 'build', '.next'].includes(item)) {
  57. scanDirectory(fullPath);
  58. }
  59. } else if (stat.isFile() && (item.endsWith('.ts') || item.endsWith('.tsx'))) {
  60. files.push(fullPath);
  61. }
  62. }
  63. }
  64. scanDirectory(dir);
  65. return files;
  66. }
  67. function checkFileForBadImports(filePath) {
  68. const content = fs.readFileSync(filePath, 'utf8');
  69. const lines = content.split('\n');
  70. const badImports = [];
  71. for (let i = 0; i < lines.length; i++) {
  72. const line = lines[i];
  73. const trimmedLine = line.trim();
  74. if (trimmedLine.startsWith('import')) {
  75. if (trimmedLine.includes('../')) {
  76. badImports.push({
  77. line: i + 1,
  78. content: trimmedLine,
  79. reason: 'Relative imports going up directories (../) are not allowed in hook files',
  80. });
  81. }
  82. if (trimmedLine.includes('@/') && !trimmedLine.includes(REQUIRED_PREFIX)) {
  83. badImports.push({
  84. line: i + 1,
  85. content: trimmedLine,
  86. reason: `Import must start with ${REQUIRED_PREFIX}`,
  87. });
  88. }
  89. }
  90. }
  91. return badImports;
  92. }
  93. function checkFileForBannedImports(filePath) {
  94. const content = fs.readFileSync(filePath, 'utf8');
  95. const lines = content.split('\n');
  96. const badImports = [];
  97. for (let i = 0; i < lines.length; i++) {
  98. const line = lines[i];
  99. const trimmedLine = line.trim();
  100. if (trimmedLine.startsWith('import') && trimmedLine.includes(BANNED_IMPORT)) {
  101. badImports.push({
  102. line: i + 1,
  103. content: trimmedLine,
  104. reason: `Import from '${BANNED_IMPORT}' is not allowed anywhere in the dashboard app`,
  105. });
  106. }
  107. }
  108. return badImports;
  109. }
  110. /**
  111. * Check for React Context module identity issues.
  112. *
  113. * Files in src/lib/ are auto-exported via index.ts. If a file defines a React Context
  114. * AND also consumes it (via useContext), this causes module identity issues when
  115. * extensions dynamically import from @vendure/dashboard - the context object in the
  116. * extension's bundle will be different from the one in the main app's bundle.
  117. *
  118. * The fix is to split context definition and consumption into separate files:
  119. * - Context definition in a dedicated file (e.g., paginated-list-context.ts)
  120. * - Hook that consumes the context in src/lib/hooks/ (e.g., use-paginated-list.ts)
  121. */
  122. function checkFileForContextPattern(filePath) {
  123. const content = fs.readFileSync(filePath, 'utf8');
  124. const issues = [];
  125. // Check if file is in src/lib/ (auto-exported)
  126. if (!filePath.includes(normalizePath('src/lib/'))) {
  127. return issues;
  128. }
  129. // Check if file is in the allowlist
  130. for (const allowedFile of CONTEXT_PATTERN_ALLOWLIST) {
  131. if (filePath.includes(normalizePath(allowedFile))) {
  132. return issues;
  133. }
  134. }
  135. const hasCreateContext = /createContext\s*[<(]/.test(content);
  136. const hasUseContext = /useContext\s*\(/.test(content);
  137. if (hasCreateContext && hasUseContext) {
  138. // Find line numbers for better error reporting
  139. const lines = content.split('\n');
  140. let createContextLine = 0;
  141. let useContextLine = 0;
  142. for (let i = 0; i < lines.length; i++) {
  143. if (/createContext\s*[<(]/.test(lines[i]) && createContextLine === 0) {
  144. createContextLine = i + 1;
  145. }
  146. if (/useContext\s*\(/.test(lines[i]) && useContextLine === 0) {
  147. useContextLine = i + 1;
  148. }
  149. }
  150. issues.push({
  151. createContextLine,
  152. useContextLine,
  153. reason:
  154. 'File defines a React Context (createContext) and also consumes it (useContext). ' +
  155. 'This causes module identity issues when extensions dynamically import from @vendure/dashboard. ' +
  156. 'Split into separate files: context definition in a dedicated file, hook in src/lib/hooks/',
  157. });
  158. }
  159. return issues;
  160. }
  161. function findLibFiles(dir) {
  162. const files = [];
  163. function scanDirectory(currentDir) {
  164. const items = fs.readdirSync(currentDir);
  165. for (const item of items) {
  166. const fullPath = normalizePath(path.join(currentDir, item));
  167. const stat = fs.statSync(fullPath);
  168. if (stat.isDirectory()) {
  169. if (!['node_modules', '.git', 'dist', 'build'].includes(item)) {
  170. scanDirectory(fullPath);
  171. }
  172. } else if (stat.isFile() && (item.endsWith('.ts') || item.endsWith('.tsx'))) {
  173. // Skip test and story files
  174. if (!item.endsWith('.spec.ts') && !item.endsWith('.stories.tsx')) {
  175. files.push(fullPath);
  176. }
  177. }
  178. }
  179. }
  180. scanDirectory(dir);
  181. return files;
  182. }
  183. function main() {
  184. console.log('🔍 Checking for import patterns in the dashboard app...\n');
  185. console.log('📁 Checking hook files (use-*.ts/tsx) in src/lib/hooks directory...');
  186. console.log('✅ Hook file requirements:');
  187. console.log(` - All imports must start with ${REQUIRED_PREFIX}`);
  188. console.log(' - Relative imports going up directories (../) are not allowed');
  189. console.log(' - Relative imports in same directory (./) are allowed');
  190. console.log('');
  191. if (!fs.existsSync(HOOKS_DIR)) {
  192. console.error('❌ src/lib/hooks directory not found!');
  193. process.exit(1);
  194. }
  195. const hookFiles = findHookFiles(HOOKS_DIR);
  196. let hasBadImports = false;
  197. let totalBadImports = 0;
  198. for (const file of hookFiles) {
  199. const relativePath = normalizePath(path.relative(process.cwd(), file));
  200. const badImports = checkFileForBadImports(file);
  201. if (badImports.length > 0) {
  202. hasBadImports = true;
  203. totalBadImports += badImports.length;
  204. console.log(`❌ ${relativePath}:`);
  205. for (const badImport of badImports) {
  206. console.log(` Line ${badImport.line}: ${badImport.content}`);
  207. console.log(` Reason: ${badImport.reason}`);
  208. }
  209. console.log('');
  210. }
  211. }
  212. if (hasBadImports) {
  213. console.log(`❌ Found ${totalBadImports} bad import(s) in ${hookFiles.length} hook file(s)`);
  214. console.log(
  215. `💡 All imports in hook files must start with ${REQUIRED_PREFIX} and must not use relative paths going up directories`,
  216. );
  217. } else {
  218. console.log(`✅ No bad imports found in ${hookFiles.length} hook file(s)`);
  219. console.log(`🎉 All imports in hook files are using ${REQUIRED_PREFIX} prefix`);
  220. }
  221. console.log('\n📁 Checking all dashboard files for banned imports...');
  222. console.log('✅ Dashboard-wide requirements:');
  223. console.log(` - Import from '${BANNED_IMPORT}' is not allowed anywhere`);
  224. console.log('');
  225. if (!fs.existsSync(DASHBOARD_SRC_DIR)) {
  226. console.error('❌ src directory not found!');
  227. process.exit(1);
  228. }
  229. const dashboardFiles = findDashboardFiles(DASHBOARD_SRC_DIR);
  230. let hasBannedImports = false;
  231. let totalBannedImports = 0;
  232. for (const file of dashboardFiles) {
  233. const relativePath = normalizePath(path.relative(process.cwd(), file));
  234. const bannedImports = checkFileForBannedImports(file);
  235. if (bannedImports.length > 0) {
  236. hasBannedImports = true;
  237. totalBannedImports += bannedImports.length;
  238. console.log(`❌ ${relativePath}:`);
  239. for (const bannedImport of bannedImports) {
  240. console.log(` Line ${bannedImport.line}: ${bannedImport.content}`);
  241. console.log(` Reason: ${bannedImport.reason}`);
  242. }
  243. console.log('');
  244. }
  245. }
  246. if (hasBannedImports) {
  247. console.log(
  248. `❌ Found ${totalBannedImports} banned import(s) in ${dashboardFiles.length} dashboard file(s)`,
  249. );
  250. console.log(`💡 Import from '${BANNED_IMPORT}' is not allowed anywhere in the dashboard app`);
  251. } else {
  252. console.log(`✅ No banned imports found in ${dashboardFiles.length} dashboard file(s)`);
  253. console.log(`🎉 All dashboard files are free of banned imports`);
  254. }
  255. // Check for React Context module identity issues in lib files
  256. console.log('\n📁 Checking src/lib files for React Context patterns...');
  257. console.log('✅ Context pattern requirements:');
  258. console.log(' - Files must NOT both define (createContext) and consume (useContext) a React Context');
  259. console.log(' - Split context definition and hooks into separate files to prevent module identity issues');
  260. console.log('');
  261. if (!fs.existsSync(LIB_DIR)) {
  262. console.error('❌ src/lib directory not found!');
  263. process.exit(1);
  264. }
  265. const libFiles = findLibFiles(LIB_DIR);
  266. let hasContextIssues = false;
  267. let totalContextIssues = 0;
  268. for (const file of libFiles) {
  269. const relativePath = normalizePath(path.relative(process.cwd(), file));
  270. const contextIssues = checkFileForContextPattern(file);
  271. if (contextIssues.length > 0) {
  272. hasContextIssues = true;
  273. totalContextIssues += contextIssues.length;
  274. console.log(`❌ ${relativePath}:`);
  275. for (const issue of contextIssues) {
  276. console.log(` createContext at line ${issue.createContextLine}, useContext at line ${issue.useContextLine}`);
  277. console.log(` Reason: ${issue.reason}`);
  278. }
  279. console.log('');
  280. }
  281. }
  282. if (hasContextIssues) {
  283. console.log(`❌ Found ${totalContextIssues} context pattern issue(s) in ${libFiles.length} lib file(s)`);
  284. console.log('💡 Move context definitions to dedicated files and hooks to src/lib/hooks/');
  285. } else {
  286. console.log(`✅ No context pattern issues found in ${libFiles.length} lib file(s)`);
  287. console.log('🎉 All lib files follow the correct context pattern');
  288. }
  289. if (hasBadImports || hasBannedImports || hasContextIssues) {
  290. process.exit(1);
  291. }
  292. }
  293. main();