import { URL } from 'node:url';
import fs from 'node:fs';
import dotenv from 'dotenv';
import watch from 'node-watch';
import { loadLanguages } from './languageLoader.js';
/**
* @typedef { import('../package.json') } CFG
*/
const replaceTemplate = /\[\[(.*?)\]\]/g;
const metaStr = '[[metadata]]';
const log = (...msg) => {
console.log('[LOG]', ...msg);
};
const err = (...msg) => {
console.error('[ERROR]', ...msg);
};
/**
* Object is typeof `object` / JSON Object
* @template O
* @param { O } obj
* @returns { boolean }
*/
const isObj = (obj) => {
/** @type { string } */
const s = Object.prototype.toString.call(obj);
return s.includes('Object');
};
/**
* Object is `null` or `undefined`
* @template O
* @param { O } obj
* @returns { boolean }
*/
const isNull = (obj) => {
return Object.is(obj, null) || Object.is(obj, undefined);
};
/**
* Object is Blank
* @template O
* @param { O } obj
* @returns { boolean }
*/
const isBlank = (obj) => {
return (
(typeof obj === 'string' && Object.is(obj.trim(), '')) ||
((obj instanceof Set || obj instanceof Map) && Object.is(obj.size, 0)) ||
(Array.isArray(obj) && Object.is(obj.length, 0)) ||
(isObj(obj) && Object.is(Object.keys(obj).length, 0))
);
};
/**
* Object is Empty
* @template O
* @param { O } obj
* @returns { boolean }
*/
const isEmpty = (obj) => {
return isNull(obj) || isBlank(obj);
};
const nano = (template, data) => {
return template.replace(replaceTemplate, (_match, key) => {
const keys = key.split('.');
let v = data[keys.shift()];
for (const i in keys.length) v = v[keys[i]];
return isEmpty(v) ? '' : v;
});
};
const canAccess = async (filePath, encoding = 'utf-8') => {
const testAccess = await fs.promises.access(
filePath,
fs.promises.constants.R_OK | fs.promises.constants.W_OK
);
if (isNull(testAccess)) {
const data = await fs.promises.readFile(filePath, encoding);
return data.toString(encoding);
}
return {
msg: `Cannot access provided filePath: ${filePath}`
};
};
const fileToJSON = async (filePath, encoding = 'utf-8') => {
const testAccess = await canAccess(filePath, encoding);
if (isObj(testAccess)) {
throw new Error(testAccess.msg);
}
return JSON.parse(testAccess);
};
const writeUserJS = async (destinationFilePath, data) => {
return await fs.promises.writeFile(destinationFilePath, data);
};
const toTime = () => {
return new Intl.DateTimeFormat('default', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
fractionalSecondDigits: 3
}).format(new Date());
};
const dataMap = new Map();
const compareArrays = (a, b) =>
a.length === b.length && a.every((element, index) => element === b[index]);
const addTo = (key, ...values) => {
if (values.length === 0) {
return '';
}
if (dataMap.has(key)) {
if (compareArrays(dataMap.get(key), values)) {
return dataMap.get(key);
}
}
dataMap.set(key, values);
return dataMap.get(key);
};
const filterData = [
'name',
'description',
'author',
'icon',
'version',
'url',
'homepage',
'bugs',
'license'
];
(async () => {
/** @type { dotenv.DotenvConfigOutput } */
let result = {};
try {
// const args = process.argv.filter((val, i) => i >= 2);
// if ('USERJS_META' in process.env) {
// args.push(process.env.USERJS_META);
// }
// if (Object.is(args.length, 0)) {
// args.push('./package.json');
// }
/**
* @type { CFG }
*/
const jsonData = await fileToJSON('./package.json');
if (!isObj(jsonData)) {
throw new Error('"jsonData" must be an object.');
}
if (!jsonData.userJS) {
throw new Error('Missing "userJS" key');
}
const userJS = jsonData.userJS;
const { build } = userJS;
result = dotenv.config({
path: isEmpty(process.env.JS_ENV) ? build.paths.dev.env : build.paths.public.env
});
if (result.error) {
throw result.error;
}
if (isNull(result.parsed.JS_ENV)) {
dotenv.populate(
result.parsed,
{
JS_ENV: 'development'
},
{ override: true, debug: true }
);
}
const env = result.parsed;
const js_env = env.JS_ENV === 'development';
const dp = js_env ? 'dev' : 'public';
const compileMetadata = () => {
const metaData = [];
for (const [key, value] of Object.entries(userJS.metadata)) {
if (Array.isArray(value)) {
for (const v of value) {
metaData.push(`// @${key} ${v}`);
}
} else if (isObj(value)) {
for (const [k, v] of Object.entries(value)) {
metaData.push(`// @${key} ${k} ${v}`);
}
} else if (typeof value === 'boolean') {
if (value === true) {
metaData.push(`// @${key}`);
}
} else {
metaData.push(`// @${key} ${value}`);
}
}
return metaData.join('\n');
};
const buildUserJS = async () => {
try {
const lngList = await loadLanguages(new URL('../src/_locales', import.meta.url));
const transformLanguages = () => {
const resp = {};
for (const obj of lngList) {
for (const [k, v] of Object.entries(obj)) {
const o = {};
for (const [key, value] of Object.entries(v)) {
if (key.startsWith('ext')) {
continue;
}
if (/userjs_(name|description)/i.test(key)) {
continue;
}
if (isEmpty(value.message)) {
continue;
}
o[key] = value.message;
}
resp[k] = o;
}
}
return JSON.stringify(resp);
};
const compileLanguage = (type = 'userjs_name') => {
const resp = [];
for (const obj of lngList) {
for (const [k, v] of Object.entries(obj)) {
if (v[type]) {
if (isEmpty(v[type].message)) {
continue;
}
if (k.startsWith('en')) {
continue;
}
const t = type.toLowerCase().replace('userjs_', '');
if (type === 'userjs_name') {
resp.push(
`// @${t}:${k.replace('_', '-')} ${js_env ? '[Dev] ' : ''}${v[type].message}`
);
} else {
resp.push(`// @${t}:${k.replace('_', '-')} ${v[type].message}`);
}
}
}
}
return resp;
};
const getData = () => {
for (const [k, v] of Object.entries(userJS)) {
if (typeof v !== 'string') {
continue;
}
if (k === 'name') {
addTo(
k,
`// @${k} ${js_env ? '[Dev] ' : ''}${v}`,
...compileLanguage('userjs_name')
);
} else if (k === 'description') {
addTo(k, `// @${k} ${v}`, ...compileLanguage('userjs_description'));
} else if (k === 'author') {
addTo(k, `// @${k} ${v}`);
} else if (k === 'icon') {
const buff = new Buffer.from(fs.readFileSync(v));
const base64data = buff.toString('base64');
if (v.endsWith('.png')) {
addTo(k, `// @${k} data:image/png;base64,${base64data}`);
} else if (v.endsWith('.svg')) {
addTo(k, `// @${k} data:image/svg+xml;base64,${base64data}`);
}
} else if (k === 'downloadURL') {
addTo(k, `// @downloadURL ${v}`);
} else if (k === 'updateURL') {
addTo(k, `// @updateURL ${v}`);
} else if (k === 'url') {
addTo(k, `// @downloadURL ${v}`, `// @updateURL ${v}`);
} else if (k === 'version') {
addTo(k, `// @${k} ${js_env ? +new Date() : v}`);
} else if (k === 'homepage') {
addTo(k, `// @namespace ${v}`, `// @homepageURL ${v}`);
} else if (k === 'bugs') {
addTo(k, `// @supportURL ${v}`);
} else if (k === 'license') {
addTo(k, `// @${k} ${v}`);
} else {
addTo(k, v);
}
}
for (const [k, v] of Object.entries(jsonData)) {
if (typeof v !== 'string') {
continue;
}
if (!filterData.includes(k)) {
continue;
}
if (dataMap.has(k)) {
continue;
}
if (k === 'license') {
addTo(k, `// @${k} ${v}`);
}
}
if (userJS.metadata) {
addTo('metaData', compileMetadata());
}
return [...dataMap.values()].flat().join('\n');
};
const userJSHeader = `// ==UserScript==\n${getData()}\n// ==/UserScript==`;
const headerFile = await canAccess(build.source.head);
const mainFile = await canAccess(build.source.body);
const nanoCFG = {
metadata: userJSHeader,
languageList: transformLanguages(),
code: mainFile
};
if (build.source.extras) {
for (const [k, v] of Object.entries(build.source.extras)) {
const extraFile = await canAccess(v);
if (typeof extraFile === 'string') {
nanoCFG[k] = extraFile;
}
}
}
const outFile = `${build.paths[dp].dir}/${build.paths[dp].fileName}.user.js`;
await writeUserJS(outFile, nano(headerFile, nanoCFG));
log('UserJS Build:', {
path: outFile,
time: toTime()
});
if (!js_env) {
const outMeta = `${build.paths[dp].dir}/${build.paths[dp].fileName}.meta.js`;
await writeUserJS(outMeta, nano(metaStr, { metadata: userJSHeader }));
log('UserJS Metadata:', {
path: outMeta,
time: toTime()
});
}
} catch (ex) {
err(ex);
}
};
const watcher = watch(build.watchDirs, {
recursive: true,
delay: 2000,
filter: /\.(js|[s]css)$/
});
//#region Start Process
log(`Node ENV: ${env.JS_ENV}`);
if (js_env) {
watcher.on('change', buildUserJS);
watcher.on('error', (ex) => {
err(ex);
watcher.close();
});
watcher.on('ready', buildUserJS);
return;
}
await buildUserJS();
process.exit(0);
//#endregion
} catch (ex) {
err(ex);
}
})();