|
| 1 | +const fs = require("fs"); |
| 2 | +const path = require("path"); |
| 3 | + |
| 4 | +// dist/ is assembled as a complete package root, not just compiled output: |
| 5 | +// lib/ resolves siblings through __dirname (../package.json, ../docs/helpers, |
| 6 | +// ../../vendor/gradle-plugin, ...), so those directories have to sit next to it |
| 7 | +// exactly as they do in the repo. |
| 8 | + |
| 9 | +const rootDir = path.join(__dirname, ".."); |
| 10 | +const distDir = path.join(rootDir, "dist"); |
| 11 | +const release = process.argv.includes("--release"); |
| 12 | + |
| 13 | +// bundleDependencies are resolved from node_modules next to the manifest being |
| 14 | +// packed, so they have to be mirrored into dist for `npm pack` to bundle them |
| 15 | +const BUNDLED = ["universal-analytics", "debug", "ms", "uuid"]; |
| 16 | + |
| 17 | +const SIBLING_DIRS = ["resources", "docs", "config", "vendor", "bin", "setup"]; |
| 18 | +// npm picks README/LICENSE/CHANGELOG up from the directory being packed, so |
| 19 | +// they have to exist inside dist or they silently drop out of the tarball |
| 20 | +const ROOT_FILES = [ |
| 21 | + "postinstall.js", |
| 22 | + "preuninstall.js", |
| 23 | + "README.md", |
| 24 | + "LICENSE", |
| 25 | + "CHANGELOG.md", |
| 26 | +]; |
| 27 | + |
| 28 | +// paths (relative to the repo root) that never ship |
| 29 | +const RELEASE_EXCLUDES = [ |
| 30 | + path.join("docs", "html"), |
| 31 | + path.join("lib", "common", "docs", "fonts"), |
| 32 | + path.join("lib", "common", "test"), |
| 33 | +]; |
| 34 | + |
| 35 | +function isExcluded(relPath) { |
| 36 | + if (!release) { |
| 37 | + return false; |
| 38 | + } |
| 39 | + return RELEASE_EXCLUDES.some( |
| 40 | + (excluded) => relPath === excluded || relPath.startsWith(excluded + path.sep) |
| 41 | + ); |
| 42 | +} |
| 43 | + |
| 44 | +let copied = 0; |
| 45 | +let skipped = 0; |
| 46 | + |
| 47 | +function copyFile(sourcePath, targetPath) { |
| 48 | + const source = fs.statSync(sourcePath); |
| 49 | + if (fs.existsSync(targetPath)) { |
| 50 | + const target = fs.statSync(targetPath); |
| 51 | + // vendor/ alone is ~31MB; re-copying it on every build is pure waste |
| 52 | + if (target.mtimeMs >= source.mtimeMs && target.size === source.size) { |
| 53 | + skipped++; |
| 54 | + return; |
| 55 | + } |
| 56 | + } |
| 57 | + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); |
| 58 | + fs.copyFileSync(sourcePath, targetPath); |
| 59 | + copied++; |
| 60 | +} |
| 61 | + |
| 62 | +function copyTree(relDir, filter) { |
| 63 | + const sourceDir = path.join(rootDir, relDir); |
| 64 | + if (!fs.existsSync(sourceDir)) { |
| 65 | + return; |
| 66 | + } |
| 67 | + |
| 68 | + for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { |
| 69 | + const relPath = path.join(relDir, entry.name); |
| 70 | + if (isExcluded(relPath)) { |
| 71 | + continue; |
| 72 | + } |
| 73 | + if (entry.isDirectory()) { |
| 74 | + copyTree(relPath, filter); |
| 75 | + } else if (!filter || filter(relPath)) { |
| 76 | + copyFile(path.join(rootDir, relPath), path.join(distDir, relPath)); |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +// Everything under lib/ that is not TypeScript is an asset: vendored scripts, |
| 82 | +// hooks, platform-tools binaries, docs helpers and test fixtures. A .js with a |
| 83 | +// sibling .ts is compiler output instead - either left over from the old |
| 84 | +// in-place build or freshly emitted into dist - and copying it would overwrite |
| 85 | +// what tsc just produced. |
| 86 | +function isCompilerOutput(relPath) { |
| 87 | + const stem = relPath.replace(/\.js\.map$/, "").replace(/\.js$/, ""); |
| 88 | + return ( |
| 89 | + (relPath.endsWith(".js") || relPath.endsWith(".js.map")) && |
| 90 | + fs.existsSync(path.join(rootDir, stem + ".ts")) |
| 91 | + ); |
| 92 | +} |
| 93 | + |
| 94 | +// Hand-written .d.ts come along too. tsc treats them as inputs and never emits |
| 95 | +// them, but the generated declarations import from them, so leaving them behind |
| 96 | +// ships types with dangling references. |
| 97 | +copyTree( |
| 98 | + "lib", |
| 99 | + (relPath) => |
| 100 | + (!relPath.endsWith(".ts") || relPath.endsWith(".d.ts")) && |
| 101 | + !isCompilerOutput(relPath) |
| 102 | +); |
| 103 | + |
| 104 | +for (const dir of SIBLING_DIRS) { |
| 105 | + copyTree(dir); |
| 106 | +} |
| 107 | + |
| 108 | +if (!release) { |
| 109 | + // fixtures the compiled tests read relative to their own location |
| 110 | + copyTree(path.join("test", "files")); |
| 111 | +} |
| 112 | + |
| 113 | +for (const file of ROOT_FILES) { |
| 114 | + copyFile(path.join(rootDir, file), path.join(distDir, file)); |
| 115 | +} |
| 116 | + |
| 117 | +for (const dep of BUNDLED) { |
| 118 | + copyTree(path.join("node_modules", dep)); |
| 119 | +} |
| 120 | + |
| 121 | +writeManifest(); |
| 122 | + |
| 123 | +function writeManifest() { |
| 124 | + const pkg = JSON.parse( |
| 125 | + fs.readFileSync(path.join(rootDir, "package.json"), "utf8") |
| 126 | + ); |
| 127 | + |
| 128 | + // dist is the package root once published, so entrypoints lose the dist/ |
| 129 | + // prefix they carry in the source manifest |
| 130 | + pkg.main = pkg.main.replace(/^\.\/dist\//, "./"); |
| 131 | + |
| 132 | + delete pkg.devDependencies; |
| 133 | + delete pkg.files; |
| 134 | + delete pkg.overrides; |
| 135 | + delete pkg["lint-staged"]; |
| 136 | + |
| 137 | + pkg.scripts = { |
| 138 | + postinstall: pkg.scripts.postinstall, |
| 139 | + preuninstall: pkg.scripts.preuninstall, |
| 140 | + }; |
| 141 | + |
| 142 | + fs.writeFileSync( |
| 143 | + path.join(distDir, "package.json"), |
| 144 | + JSON.stringify(pkg, null, 2) + "\n" |
| 145 | + ); |
| 146 | +} |
| 147 | + |
| 148 | +console.log( |
| 149 | + `assets: ${copied} copied, ${skipped} up to date${release ? " (release)" : ""}` |
| 150 | +); |
0 commit comments