|
const fs = require("fs"); |
|
const path = require("path"); |
|
const { spawnSync } = require("child_process"); |
|
|
|
const PRESETS = { |
|
"3.7.1": { |
|
insertAfter: "jQuery.isArray = Array.isArray;\n", |
|
callAnchor: "jQuery.noConflict = function( deep ) {" |
|
}, |
|
"3.7": { |
|
insertAfter: "jQuery.isArray = Array.isArray;\n", |
|
callAnchor: "jQuery.noConflict = function( deep ) {" |
|
}, |
|
"4.0.0": { |
|
insertAfter: "jQuery.expr[ \":\" ] = jQuery.expr.filters = jQuery.expr.pseudos;\n", |
|
callAnchor: "jQuery.noConflict = function( deep ) {" |
|
}, |
|
"4.0": { |
|
insertAfter: "jQuery.expr[ \":\" ] = jQuery.expr.filters = jQuery.expr.pseudos;\n", |
|
callAnchor: "jQuery.noConflict = function( deep ) {" |
|
} |
|
}; |
|
|
|
function parseArgs(argv) { |
|
const args = {}; |
|
for (let i = 0; i < argv.length; i++) { |
|
const current = argv[i]; |
|
if (!current.startsWith("--")) { |
|
continue; |
|
} |
|
|
|
const key = current.slice(2); |
|
const next = argv[i + 1]; |
|
if (!next || next.startsWith("--")) { |
|
args[key] = true; |
|
continue; |
|
} |
|
|
|
args[key] = next; |
|
i++; |
|
} |
|
return args; |
|
} |
|
|
|
function printHelp() { |
|
console.log(`Usage: |
|
node embed-js.js ^ |
|
--jquery ./jquery-4.0.0.js ^ |
|
--payload ./ywbackfix.js ^ |
|
--method setColorScheme ^ |
|
--minify |
|
|
|
or: |
|
|
|
node embed-js.js ^ |
|
--svg ./banner.svg ^ |
|
--payload ./hello.js |
|
|
|
Required: |
|
--payload Path to JS payload that will be embedded |
|
--jquery Path to source jQuery file |
|
--svg Path to source SVG file |
|
|
|
Only one of --jquery or --svg may be used. |
|
|
|
Optional for jQuery: |
|
--method jQuery method name to create and call |
|
--preset One of: 3.7.1, 3.7, 4.0.0, 4.0 |
|
--version-label Override label written into the diagnostic marker |
|
--insert-after Manual insertion anchor override |
|
--call-anchor Manual call anchor override |
|
--minify Also build <auto-output>.min.js with terser |
|
--no-marker Disable diagnostic window.__YWBJqueryEmbed marker |
|
|
|
Output: |
|
The helper builds the output path automatically: |
|
<source-name>.<payload-name><source-ext> |
|
|
|
Examples: |
|
jquery-4.0.0.js + ywbackfix.js -> jquery-4.0.0.ywbackfix.js |
|
banner.svg + hello.js -> banner.hello.svg |
|
`); |
|
} |
|
|
|
function ensureString(value, name) { |
|
if (!value || typeof value !== "string") { |
|
throw new Error(`Missing required argument: ${name}`); |
|
} |
|
} |
|
|
|
function assertContains(haystack, needle, label) { |
|
if (!haystack.includes(needle)) { |
|
throw new Error(`Anchor not found for ${label}: ${needle}`); |
|
} |
|
} |
|
|
|
function detectJQueryVersion(source, jqueryPath) { |
|
const bannerMatch = source.match(/jQuery JavaScript Library v([0-9]+\.[0-9]+(?:\.[0-9]+)?)/); |
|
if (bannerMatch) { |
|
return bannerMatch[1]; |
|
} |
|
|
|
const fileMatch = path.basename(jqueryPath).match(/jquery-([0-9]+\.[0-9]+(?:\.[0-9]+)?)/i); |
|
if (fileMatch) { |
|
return fileMatch[1]; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
function detectPresetKey(version) { |
|
if (!version) { |
|
return null; |
|
} |
|
|
|
if (PRESETS[version]) { |
|
return version; |
|
} |
|
|
|
const majorMinor = version.split(".").slice(0, 2).join("."); |
|
if (PRESETS[majorMinor]) { |
|
return majorMinor; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
function deriveOutputPath(sourcePath, payloadPath) { |
|
const parsedSource = path.parse(sourcePath); |
|
const payloadStem = path.parse(payloadPath).name; |
|
return path.join(parsedSource.dir, `${parsedSource.name}.${payloadStem}${parsedSource.ext}`); |
|
} |
|
|
|
function indentPayload(rawScript) { |
|
return rawScript |
|
.split(/\r?\n/) |
|
.map((line) => `\t${line}`) |
|
.join("\n"); |
|
} |
|
|
|
function buildWrappedPayload(rawScript, options) { |
|
const lines = ["", `jQuery.${options.methodName} = function() {`]; |
|
|
|
if (options.includeMarker) { |
|
lines.push("\twindow.__YWBJqueryEmbed = window.__YWBJqueryEmbed || [];"); |
|
lines.push( |
|
`\twindow.__YWBJqueryEmbed.push({ version: ${JSON.stringify(options.versionLabel)}, ts: Date.now() });` |
|
); |
|
} |
|
|
|
lines.push(indentPayload(rawScript)); |
|
lines.push("};", ""); |
|
return lines.join("\n"); |
|
} |
|
|
|
function buildCallBlock(methodName) { |
|
return ["", `jQuery.${methodName}();`, ""].join("\n"); |
|
} |
|
|
|
function buildEmbeddedJQuery(options) { |
|
const source = fs.readFileSync(options.sourcePath, "utf8"); |
|
const rawScript = fs.readFileSync(options.payloadPath, "utf8").trimEnd(); |
|
|
|
assertContains(source, options.insertAfter, options.sourcePath); |
|
assertContains(source, options.callAnchor, options.sourcePath); |
|
|
|
const withMethod = source.replace( |
|
options.insertAfter, |
|
`${options.insertAfter}${buildWrappedPayload(rawScript, options)}` |
|
); |
|
|
|
return withMethod.replace( |
|
options.callAnchor, |
|
`${buildCallBlock(options.methodName)}${options.callAnchor}` |
|
); |
|
} |
|
|
|
function buildEmbeddedSvg(options) { |
|
const source = fs.readFileSync(options.sourcePath, "utf8"); |
|
const rawScript = fs.readFileSync(options.payloadPath, "utf8").trimEnd(); |
|
const closingTagMatch = source.match(/<\/svg>\s*$/i); |
|
|
|
if (!closingTagMatch) { |
|
throw new Error(`Closing </svg> tag not found in ${options.sourcePath}`); |
|
} |
|
|
|
const scriptBlock = [ |
|
"", |
|
"<script><![CDATA[", |
|
rawScript, |
|
"]]></script>", |
|
"" |
|
].join("\n"); |
|
|
|
return source.replace(/<\/svg>\s*$/i, `${scriptBlock}</svg>\n`); |
|
} |
|
|
|
function buildMinifiedOutput(outputPath) { |
|
const minPath = outputPath.replace(/\.js$/i, ".min.js"); |
|
const result = process.platform === "win32" |
|
? spawnSync( |
|
"cmd.exe", |
|
[ |
|
"/d", |
|
"/s", |
|
"/c", |
|
`npx terser ${path.basename(outputPath)} --compress --mangle --output ${path.basename(minPath)}` |
|
], |
|
{ |
|
stdio: "inherit", |
|
cwd: path.dirname(outputPath) |
|
} |
|
) |
|
: spawnSync( |
|
"npx", |
|
["terser", outputPath, "--compress", "--mangle", "--output", minPath], |
|
{ stdio: "inherit" } |
|
); |
|
|
|
if (result.error) { |
|
throw result.error; |
|
} |
|
|
|
if (result.status !== 0) { |
|
throw new Error(`Terser failed for ${outputPath}`); |
|
} |
|
|
|
return minPath; |
|
} |
|
|
|
function resolveOptions(rawArgs) { |
|
ensureString(rawArgs.payload, "--payload"); |
|
|
|
const hasJQuery = typeof rawArgs.jquery === "string"; |
|
const hasSvg = typeof rawArgs.svg === "string"; |
|
|
|
if (hasJQuery === hasSvg) { |
|
throw new Error("Use exactly one of --jquery or --svg"); |
|
} |
|
|
|
const mode = hasJQuery ? "jquery" : "svg"; |
|
const sourcePath = path.resolve(hasJQuery ? rawArgs.jquery : rawArgs.svg); |
|
const payloadPath = path.resolve(rawArgs.payload); |
|
const outputPath = deriveOutputPath(sourcePath, payloadPath); |
|
|
|
if (mode === "svg") { |
|
return { |
|
mode, |
|
sourcePath, |
|
payloadPath, |
|
outputPath, |
|
methodName: null, |
|
versionLabel: null, |
|
detectedVersion: null, |
|
presetKey: null, |
|
insertAfter: null, |
|
callAnchor: null, |
|
includeMarker: false, |
|
minify: false |
|
}; |
|
} |
|
|
|
const source = fs.readFileSync(sourcePath, "utf8"); |
|
const detectedVersion = detectJQueryVersion(source, sourcePath); |
|
const presetKey = rawArgs.preset || detectPresetKey(detectedVersion); |
|
const preset = presetKey ? PRESETS[presetKey] : null; |
|
const methodName = rawArgs.method || "setColorScheme"; |
|
const versionLabel = |
|
rawArgs["version-label"] || detectedVersion || presetKey || path.basename(sourcePath); |
|
const insertAfter = rawArgs["insert-after"] || (preset && preset.insertAfter); |
|
const callAnchor = rawArgs["call-anchor"] || (preset && preset.callAnchor); |
|
|
|
ensureString(insertAfter, "--insert-after or --preset"); |
|
ensureString(callAnchor, "--call-anchor or --preset"); |
|
|
|
return { |
|
mode, |
|
sourcePath, |
|
payloadPath, |
|
outputPath, |
|
methodName, |
|
versionLabel, |
|
detectedVersion, |
|
presetKey, |
|
insertAfter, |
|
callAnchor, |
|
includeMarker: rawArgs["no-marker"] !== true, |
|
minify: rawArgs.minify === true |
|
}; |
|
} |
|
|
|
function run(rawArgs) { |
|
const options = resolveOptions(rawArgs); |
|
const output = options.mode === "jquery" |
|
? buildEmbeddedJQuery(options) |
|
: buildEmbeddedSvg(options); |
|
|
|
fs.writeFileSync(options.outputPath, output, "utf8"); |
|
|
|
const result = { |
|
mode: options.mode, |
|
output: options.outputPath, |
|
minified: null, |
|
method: options.methodName, |
|
detectedVersion: options.detectedVersion, |
|
preset: options.presetKey, |
|
versionLabel: options.versionLabel |
|
}; |
|
|
|
if (options.mode === "jquery" && options.minify) { |
|
result.minified = buildMinifiedOutput(options.outputPath); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
if (require.main === module) { |
|
const args = parseArgs(process.argv.slice(2)); |
|
if (args.help) { |
|
printHelp(); |
|
process.exit(0); |
|
} |
|
|
|
try { |
|
const result = run(args); |
|
console.log(JSON.stringify(result, null, 2)); |
|
} catch (error) { |
|
console.error(error.message); |
|
process.exit(1); |
|
} |
|
} |
|
|
|
module.exports = { |
|
PRESETS, |
|
buildEmbeddedJQuery, |
|
buildEmbeddedSvg, |
|
detectJQueryVersion, |
|
detectPresetKey, |
|
deriveOutputPath, |
|
parseArgs, |
|
printHelp, |
|
resolveOptions, |
|
run |
|
}; |