analytics/tracker/compiler/index.js

176 lines
4.5 KiB
JavaScript

import { minifySync } from '@swc/core'
import { rollup } from 'rollup'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import variantsFile from './variants.json' with { type: 'json' }
import { canSkipCompile } from './can-skip-compile.js'
import packageJson from '../package.json' with { type: 'json' }
import progress from 'cli-progress'
import { spawn, Worker, Pool } from 'threads'
import json from '@rollup/plugin-json'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export const DEFAULT_GLOBALS = {
COMPILE_PLAUSIBLE_WEB: false,
COMPILE_PLAUSIBLE_NPM: false,
COMPILE_PLAUSIBLE_LEGACY_VARIANT: false,
COMPILE_CONFIG: false,
COMPILE_HASH: false,
COMPILE_OUTBOUND_LINKS: false,
COMPILE_COMPAT: false,
COMPILE_LOCAL: false,
COMPILE_MANUAL: false,
COMPILE_FILE_DOWNLOADS: false,
COMPILE_PAGEVIEW_PROPS: false,
COMPILE_CUSTOM_PROPERTIES: false,
COMPILE_TAGGED_EVENTS: false,
COMPILE_REVENUE: false,
COMPILE_EXCLUSIONS: false,
COMPILE_TRACKER_SCRIPT_VERSION: packageJson.tracker_script_version
}
const ALL_VARIANTS = variantsFile.legacyVariants.concat(
variantsFile.manualVariants
)
function shouldCompileVariant(variant) {
const IS_CE = ['ce', 'ce_test', 'ce_dev'].includes(process.env.MIX_ENV)
const shouldCompileVariant = IS_CE && variant.ee_only ? false : true
return shouldCompileVariant
}
export async function compileAll(options = {}) {
if (process.env.NODE_ENV === 'dev' && canSkipCompile()) {
console.info(
'COMPILATION SKIPPED: No changes detected in tracker dependencies'
)
return
}
const bundledCode = await bundleCode()
const startTime = Date.now()
const variantsToCompile = ALL_VARIANTS.filter(shouldCompileVariant)
console.log(`Starting compilation of ${variantsToCompile.length} variants...`)
const bar = new progress.SingleBar(
{ clearOnComplete: true },
progress.Presets.shades_classic
)
bar.start(variantsToCompile.length, 0)
const workerPool = Pool(() => spawn(new Worker('./worker-thread.js')))
variantsToCompile.forEach((variant) => {
workerPool.queue(async (worker) => {
await worker.compileFile(variant, { ...options, bundledCode })
bar.increment()
})
})
await workerPool.completed()
await workerPool.terminate()
bar.stop()
console.log(
`Completed compilation of ${variantsToCompile.length} variants in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
)
}
export async function compileFile(variant, options) {
const globals = { ...DEFAULT_GLOBALS, ...variant.globals }
let code
if (variant.entry_point) {
code = await bundleCode(variant.entry_point)
} else {
code = options.bundledCode || (await bundleCode())
}
if (!variant.npm_package) {
code = wrapInstantlyEvaluatingFunction(code)
}
code = minify(code, globals, variant)
if (variant.npm_package) {
code = addExports(code)
}
if (options.returnCode) {
return code
} else {
fs.writeFileSync(outputPath(variant, options), code)
}
}
function wrapInstantlyEvaluatingFunction(baseCode) {
return `(function(){${baseCode}})()`
}
// Works around minification limitation of swc not allowing exports
function addExports(code) {
return `${code}\nexport { init, track, DEFAULT_FILE_TYPES }`
}
export function compileWebSnippet() {
const code = fs.readFileSync(relPath('../src/web-snippet.js')).toString()
return `
<script>
${minify(code)}
plausible.init()
</script>
`
}
async function bundleCode(entryPoint = 'src/plausible.js') {
const bundle = await rollup({
input: entryPoint,
plugins: [json({ compact: true })]
})
const { output } = await bundle.generate({ format: 'esm' })
return output[0].code
}
function outputPath(variant, options) {
if (variant.output_path) {
return relPath(`../../${variant.output_path}${options.suffix || ''}`)
} else if (variant.npm_package) {
return relPath(`../${variant.name}${options.suffix || ''}`)
} else {
return relPath(
`../../priv/tracker/js/${variant.name}${options.suffix || ''}`
)
}
}
function minify(code, globals, variant = {}) {
const minifyOptions = {
compress: {
global_defs: globals,
passes: 4
},
mangle: {}
}
if (variant.npm_package) {
minifyOptions.mangle.reserved = ['init', 'track', 'DEFAULT_FILE_TYPES']
minifyOptions.mangle.toplevel = true
}
const result = minifySync(code, minifyOptions)
if (result.code) {
return result.code
} else {
throw result.error
}
}
function relPath(segment) {
return path.join(__dirname, segment)
}