|
| 1 | +/** |
| 2 | + * Script to rewrite imports in package files to use absolute CDN URLs |
| 3 | + * |
| 4 | + * This allows bare imports like 'import { html } from "lit"' to work |
| 5 | + * without needing explicit importmap entries for every nested dependency. |
| 6 | + */ |
| 7 | + |
| 8 | +import fs from 'fs'; |
| 9 | +import path from 'path'; |
| 10 | +import { fileURLToPath } from 'url'; |
| 11 | +import { build } from 'esbuild'; |
| 12 | +import { glob } from 'glob'; |
| 13 | + |
| 14 | +const __filename = fileURLToPath(import.meta.url); |
| 15 | +const __dirname = path.dirname(__filename); |
| 16 | + |
| 17 | +// Access environment variables |
| 18 | +const version = process.env.VERSION; |
| 19 | +const cdnUrl = process.env.CDN_URL || 'https://cdn.semantic-ui.com'; |
| 20 | + |
| 21 | +// Target directory to process |
| 22 | +const argv = process.argv.slice(2); |
| 23 | +const targetDir = argv[0]; |
| 24 | + |
| 25 | +if (!targetDir) { |
| 26 | + console.error('No target directory specified'); |
| 27 | + process.exit(1); |
| 28 | +} |
| 29 | + |
| 30 | +// Validate directories exist |
| 31 | +if (!fs.existsSync(targetDir)) { |
| 32 | + console.error(`Target directory does not exist: ${targetDir}`); |
| 33 | + process.exit(1); |
| 34 | +} |
| 35 | + |
| 36 | +// Config |
| 37 | +const tmpDir = path.join(process.cwd(), 'tmp-cdn-build'); |
| 38 | + |
| 39 | +// Ensure tmp dir exists |
| 40 | +if (!fs.existsSync(tmpDir)) { |
| 41 | + fs.mkdirSync(tmpDir, { recursive: true }); |
| 42 | +} |
| 43 | + |
| 44 | +// Get package info from directory path |
| 45 | +function getPackageInfo(filePath) { |
| 46 | + // Extract package info from path |
| 47 | + // Expected format: cdn/@scope/package/version/... |
| 48 | + const parts = filePath.split(path.sep); |
| 49 | + const cdnIndex = parts.findIndex(part => part === 'cdn'); |
| 50 | + |
| 51 | + if (cdnIndex === -1 || parts.length < cdnIndex + 4) { |
| 52 | + return null; |
| 53 | + } |
| 54 | + |
| 55 | + const scope = parts[cdnIndex + 1].startsWith('@') ? parts[cdnIndex + 1] : null; |
| 56 | + const packageName = scope ? parts[cdnIndex + 2] : parts[cdnIndex + 1]; |
| 57 | + const packageVersion = scope ? parts[cdnIndex + 3] : parts[cdnIndex + 2]; |
| 58 | + |
| 59 | + return { |
| 60 | + scope, |
| 61 | + packageName, |
| 62 | + packageVersion, |
| 63 | + fullName: scope ? `${scope}/${packageName}` : packageName |
| 64 | + }; |
| 65 | +} |
| 66 | + |
| 67 | +// Extract the root path of a package from a file path |
| 68 | +function extractPackageRootPath(filePath, packageInfo) { |
| 69 | + // Extract the root package directory path for a given file |
| 70 | + // Expected format: cdn/@scope/package/version/... |
| 71 | + if (!packageInfo) return null; |
| 72 | + |
| 73 | + const parts = filePath.split(path.sep); |
| 74 | + const cdnIndex = parts.findIndex(part => part === 'cdn'); |
| 75 | + |
| 76 | + if (cdnIndex === -1) return null; |
| 77 | + |
| 78 | + let packagePathLength; |
| 79 | + if (packageInfo.scope) { |
| 80 | + // @scope/package/version - need 3 parts after cdn |
| 81 | + packagePathLength = cdnIndex + 4; |
| 82 | + } else { |
| 83 | + // package/version - need 2 parts after cdn |
| 84 | + packagePathLength = cdnIndex + 3; |
| 85 | + } |
| 86 | + |
| 87 | + if (parts.length < packagePathLength) return null; |
| 88 | + |
| 89 | + return parts.slice(0, packagePathLength).join(path.sep); |
| 90 | +} |
| 91 | + |
| 92 | +// Detects if a file is likely ESM or CommonJS |
| 93 | +function detectModuleFormat(filePath) { |
| 94 | + try { |
| 95 | + const content = fs.readFileSync(filePath, 'utf-8'); |
| 96 | + |
| 97 | + // Look for ESM indicators |
| 98 | + const hasImportExport = /\b(import|export)\b/.test(content); |
| 99 | + const hasModuleExports = /\b(module\.exports|exports\.\w+)\b/.test(content); |
| 100 | + const hasRequire = /\brequire\s*\(/.test(content); |
| 101 | + |
| 102 | + // If the file has import/export statements, it's likely ESM |
| 103 | + if (hasImportExport && !hasModuleExports) { |
| 104 | + return 'esm'; |
| 105 | + } |
| 106 | + |
| 107 | + // If it has module.exports or require, it's likely CommonJS |
| 108 | + if ((hasModuleExports || hasRequire) && !hasImportExport) { |
| 109 | + return 'commonjs'; |
| 110 | + } |
| 111 | + |
| 112 | + // If it has both or neither, prefer ESM for .mjs files, otherwise default to ESM |
| 113 | + if (filePath.endsWith('.mjs')) { |
| 114 | + return 'esm'; |
| 115 | + } |
| 116 | + |
| 117 | + // Default to ESM for modern packages |
| 118 | + return 'esm'; |
| 119 | + } catch (error) { |
| 120 | + console.error(`Error detecting module format for ${filePath}:`, error); |
| 121 | + // Default to ESM if detection fails |
| 122 | + return 'esm'; |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +// Rewrite imports in a file |
| 127 | +async function rewriteFile(filePath) { |
| 128 | + try { |
| 129 | + // Skip non-JS files |
| 130 | + if (!filePath.endsWith('.js') && !filePath.endsWith('.mjs')) { |
| 131 | + return; |
| 132 | + } |
| 133 | + |
| 134 | + // Get package info from file path |
| 135 | + const packageInfo = getPackageInfo(filePath); |
| 136 | + if (!packageInfo) { |
| 137 | + console.warn(`Could not determine package info for ${filePath}`); |
| 138 | + return; |
| 139 | + } |
| 140 | + |
| 141 | + // Detect module format |
| 142 | + const format = detectModuleFormat(filePath); |
| 143 | + console.log(`Detected ${format} format for ${path.basename(filePath)}`); |
| 144 | + |
| 145 | + // Generate output file path (preserving directory structure) |
| 146 | + const relativePath = path.relative(targetDir, filePath); |
| 147 | + const outputPath = path.join(tmpDir, relativePath); |
| 148 | + |
| 149 | + // Ensure output directory exists |
| 150 | + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); |
| 151 | + |
| 152 | + console.log(`Setting up esbuild for file: ${filePath}`); |
| 153 | + console.log(`Output path: ${outputPath}`); |
| 154 | + console.log(`Module format: ${format}`); |
| 155 | + |
| 156 | + // Use esbuild to transform the file |
| 157 | + await build({ |
| 158 | + entryPoints: [filePath], |
| 159 | + outfile: outputPath, |
| 160 | + bundle: false, |
| 161 | + format: format, // Use the detected format (esm or commonjs) |
| 162 | + write: true, |
| 163 | + logLevel: 'info', // Increase log level for debugging |
| 164 | + logLevel: 'warning' // Reduce noise in logs |
| 165 | + }); |
| 166 | + |
| 167 | + console.log(`Processed: ${filePath} -> ${outputPath}`); |
| 168 | + |
| 169 | + // Copy the output back to the original location |
| 170 | + fs.copyFileSync(outputPath, filePath); |
| 171 | + |
| 172 | + } catch (error) { |
| 173 | + console.error(`Error processing ${filePath}:`, error); |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +// Process all JS files in the target directory |
| 178 | +async function processDirectory() { |
| 179 | + try { |
| 180 | + console.log(`Starting processDirectory with target: ${targetDir}`); |
| 181 | + |
| 182 | + // Ensure the directory is absolute |
| 183 | + const absoluteTargetDir = path.resolve(targetDir); |
| 184 | + console.log(`Absolute target directory: ${absoluteTargetDir}`); |
| 185 | + |
| 186 | + // Check if directory exists |
| 187 | + if (!fs.existsSync(absoluteTargetDir)) { |
| 188 | + console.error(`ERROR: Target directory does not exist: ${absoluteTargetDir}`); |
| 189 | + return; |
| 190 | + } |
| 191 | + |
| 192 | + console.log(`Looking for JS files in: ${absoluteTargetDir}`); |
| 193 | + |
| 194 | + // Find all JS files (ensure we're handling .mjs files too) |
| 195 | + const jsFiles = glob.sync('**/*.{js,mjs}', { |
| 196 | + cwd: absoluteTargetDir, |
| 197 | + absolute: true |
| 198 | + }); |
| 199 | + |
| 200 | + console.log(`Glob pattern used: **/*.{js,mjs}`); |
| 201 | + console.log(`Files found by glob: ${jsFiles.length}`); |
| 202 | + |
| 203 | + console.log(`Found ${jsFiles.length} JS files to process in ${absoluteTargetDir}`); |
| 204 | + |
| 205 | + if (jsFiles.length === 0) { |
| 206 | + console.warn(`No JS files found in ${absoluteTargetDir}. Check if the directory is correct.`); |
| 207 | + return; // Exit early if no files found |
| 208 | + } |
| 209 | + |
| 210 | + // Log the first few files to help with debugging |
| 211 | + console.log("Sample files to process:"); |
| 212 | + jsFiles.slice(0, 3).forEach(file => console.log(` - ${file}`)); |
| 213 | + |
| 214 | + // Process files in parallel with a concurrency limit |
| 215 | + const concurrency = 10; |
| 216 | + let processedCount = 0; |
| 217 | + |
| 218 | + for (let i = 0; i < jsFiles.length; i += concurrency) { |
| 219 | + const batch = jsFiles.slice(i, i + concurrency); |
| 220 | + await Promise.all(batch.map(async file => { |
| 221 | + await rewriteFile(file); |
| 222 | + processedCount++; |
| 223 | + })); |
| 224 | + |
| 225 | + // Log progress for large directories |
| 226 | + if (jsFiles.length > 20 && (i + concurrency) % 50 === 0) { |
| 227 | + console.log(`Processed ${processedCount}/${jsFiles.length} files...`); |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + console.log(`Import rewriting completed. Processed ${processedCount} files.`); |
| 232 | + } catch (error) { |
| 233 | + console.error('Error processing directory:', error); |
| 234 | + process.exit(1); |
| 235 | + } finally { |
| 236 | + // Clean up tmp directory |
| 237 | + fs.rmSync(tmpDir, { recursive: true, force: true }); |
| 238 | + } |
| 239 | +} |
| 240 | + |
| 241 | +// Run the script |
| 242 | +processDirectory(); |
0 commit comments