import resolve from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; import copy from 'rollup-plugin-copy'; import fs from 'fs'; import path from 'path'; import { version } from './package.json'; function install(options = {}) { const sourcePath = options.sourcePath || 'source/'; const devPath = options.devPath || 'dev/'; const minPath = options.minPath || 'min/'; // Helper: parse CSS with variable substitutions function parseCss(css, values) { return css.replace(/\$(\w+)(\[\d+\])?/g, (match, key, index) => { let value = values[key]; if (index) { const idx = parseInt(index.slice(1, -1)); value = value[idx]; } if (typeof value === 'string') { return value; } // Handle numeric arrays (e.g., RGB or RGBA) if (Array.isArray(value[0])) { // Already nested arrays return value .map(vals => `rgb${vals.length === 4 ? 'a' : ''}(${vals.join(', ')})`) .join(', '); } else { // Single array return `rgb${value.length === 4 ? 'a' : ''}(${value.join(', ')})`; } }); } function readJsonc(filePath) { const jsoncText = fs.readFileSync(filePath, 'utf-8'); let text = jsoncText.replace(/\/\/.*$/gm, ''); // Remove single-line comments text = text.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments text = text.replace(/,\s*(?=[}\]])/g, ''); // Remove trailing commas in objects and arrays return JSON.parse(text); } function writeFile(filePath, data) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, typeof data === 'string' ? data : data.toString('utf-8')); return data.length; } function formatColor(rgb) { return '#' + rgb.map(c => c.toString(16).padStart(2, '0').toUpperCase()).join(''); } function writeBundle() { const themesDir = path.join(sourcePath, 'UI', 'themes'); const themes = fs.readdirSync(themesDir).filter(name => !['.', '_'].includes(name[0])); const themeData = {}; for (const theme of themes) { const themeJsonPath = path.join(themesDir, theme, 'json', 'theme.jsonc'); themeData[theme] = readJsonc(themeJsonPath); themeData[theme].themeClass = 'OxTheme' + theme[0].toUpperCase() + theme.slice(1); } const cssPath = path.join(sourcePath, 'UI', 'css', 'theme.css'); const css = fs.readFileSync(cssPath, 'utf-8') for (const theme of themes) { let themeCss = parseCss(css, themeData[theme]); themeCss = themeCss.replace(/\.png\)/g, `.png?${version})`); writeFile(path.join(devPath, 'UI', 'themes', theme, 'css', 'theme.css'), themeCss); writeFile(path.join(minPath, 'UI', 'themes', theme, 'css', 'theme.css'), themeCss); } const uiImages = {} const svgDir = path.join(sourcePath, 'UI', 'svg'); const svgs = fs.readdirSync(svgDir).filter(name => !['.', '_'].includes(name[0])); for (const filename of svgs) { const svgPath = path.join(svgDir, filename); let svg = fs.readFileSync(svgPath, 'utf-8') svg = svg.replace(/\n\s*/g, ''); svg = svg.replace(//g, ''); uiImages[filename.slice(0, -4)] = svg if (filename.startsWith('symbolLoading')) { for (const theme of themes) { let themeSVG = svg.replace(/#808080/g, formatColor(themeData[theme]['symbolDefaultColor'])) writeFile(path.join(devPath, 'UI', 'themes', theme, 'svg', filename), themeSVG); writeFile(path.join(minPath, 'UI', 'themes', theme, 'svg', filename), themeSVG); } } } } return { name: 'install-plugin', writeBundle }; } function symlinkDevPlugin(options = {}) { const sourcePath = options.source || 'source'; const devPath = options.dev || 'dev'; function shouldInclude(filePath, filename) { if (filePath.includes('_')) return false; if (filename.startsWith('.') || filename.startsWith('_')) return false; if (filename.endsWith('~')) return false; if (filePath.includes(`${path.sep}UI${path.sep}svg`)) return false; return true; } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function removeIfExists(target) { if (fs.existsSync(target) || fs.lstatSync(target, { throwIfNoEntry: false })) { try { const stat = fs.lstatSync(target); if (stat.isSymbolicLink()) { fs.unlinkSync(target); } else if (stat.isDirectory()) { fs.rmdirSync(target); } else { console.log("not symlink, what to do?", target) } } catch (err) { // ignore if it doesn't exist anymore } } } function walk(dir) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { walk(fullPath); } else if (entry.isFile()) { if (!shouldInclude(dir, entry.name)) continue; const relativePath = path.relative(sourcePath, dir); const targetDir = path.join(devPath, relativePath); const targetPath = path.join(targetDir, entry.name); const relativeSource = path.relative(targetDir, fullPath); ensureDir(targetDir); removeIfExists(targetPath); if (!fs.existsSync(targetPath)) { fs.symlinkSync(relativeSource, targetPath); } } } } return { name: 'symlink-dev-plugin', writeBundle() { if (!fs.existsSync(sourcePath)) { this.warn(`Source path "${sourcePath}" does not exist.`); return; } walk(sourcePath); } }; } // TBD: get version // TBD: add ' OxJS %s (c) %s 0x2620, dual-licensed GPL/MIT, see https://oxjs.org for details ' % (version, year) // /* // kind of inline now, but missing cache busting! # Ox.UI CSS css = read_text(source_path + 'UI/css/UI.css') css = css.replace('$import', '\n'.join([ '@import url("../themes/%s/css/theme.css?%s");' % (theme, version) for theme in themes ])) write_file('%sUI/css/UI.css' % dev_path, css) write_file('%sUI/css/UI.css' % min_path, css) # Ox.UI SVGs ui_images = {} path = source_path + 'UI/svg/' for filename in [filename for filename in os.listdir(path) if not filename[0] in '._']: svg = read_text(path + filename) svg = re.sub(r'\n\s*', '', svg) svg = re.sub(r'', '', svg) # end fix ui_images[filename[:-4]] = svg if filename.startswith('symbolLoading'): for theme in themes: theme_svg = re.sub(r'#808080', format_hex(theme_data[theme]['symbolDefaultColor']), svg) write_file('%sUI/themes/%s/svg/%s' % (dev_path, theme, filename), theme_svg) write_file('%sUI/themes/%s/svg/%s' % (min_path, theme, filename), theme_svg) write_file(min_path + 'UI/json/UI.json', json.dumps({ 'files': sorted(ui_files['min']), 'images': ui_images }, sort_keys=True)) js = re.sub( r'Ox.LOCALES = \{\}', 'Ox.LOCALES = ' + json.dumps(locales, indent=4, sort_keys=True), js ) js = re.sub( r"Ox.VERSION = '([\d\.]+)'", "Ox.VERSION = '%s'" % version, js ) */ export default { input: { 'Ox': 'source/Ox/Ox.js', 'UI': 'source/UI/UI.js', 'Unicode': 'source/Unicode/Unicode.js', 'Geo': 'source/Geo/Geo.js', 'Image': 'source/Image/Image.js', }, output: { dir: 'min', format: 'es', entryFileNames: '[name]/[name].js', chunkFileNames: '[name]/[name].js', }, plugins: [ resolve(), terser(), copy({ targets: [ { src: "source/Ox.js", dest: 'min/' }, { src: "source/Ox/json/locale.*.json", dest: 'min/Ox/json' }, { src: "source/UI/css/*.css", dest: 'min/UI/css' }, { src: "source/UI/json/locale.*.json", dest: 'min/UI/json' }, { src: "source/UI/json/UI.json", dest: 'min/UI/json/' }, // FIXME: this one should be genreated first { src: "source/UI/png", dest: 'min/UI/' }, { src: "source/UI/jquery/*.js", dest: 'min/UI/jquery' }, { src: "source/UI/themes", dest: 'min/UI' }, { src: "source/Unicode/json/*.json", dest: 'min/Unicode/json' }, { src: "source/Geo/json/*.json", dest: 'min/Geo/json' }, { src: "source/Geo/png/flags", dest: 'min/Geo/png/'} ], hook: 'writeBundle' }), install(), symlinkDevPlugin() ] };