Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions bin/build-ckeditor-styles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/* eslint-env node */

import fs from 'fs/promises';
import path from 'path';

const core = process.env.OPENPROJECT_CORE;

if (!core) {
throw new Error("Expected OPENPROJECT_CORE to be present, but wasn't.");
}

const cwd = process.cwd();
const packageJsonPath = path.join(cwd, 'package.json');
const outputPath = path.join(core, 'frontend', 'src', 'vendor', 'ckeditor', 'ckeditor.css');

const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
const dependencyNames = new Set([
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {}),
]);

const ckeditorPackages = Array
.from(dependencyNames)
.filter((name) => name.startsWith('@ckeditor/ckeditor5-'));
const ckeditorPackageSet = new Set(ckeditorPackages);

const packageCache = new Map();
const orderedPackages = [];
const visitingPackages = new Set();
const visitedPackages = new Set();

async function readPackageJson(packageName) {
if (packageCache.has(packageName)) {
return packageCache.get(packageName);
}

const packagePath = path.join(cwd, 'node_modules', packageName, 'package.json');
const pkgJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
packageCache.set(packageName, pkgJson);

return pkgJson;
}

async function visit(packageName) {
if (visitedPackages.has(packageName)) {
return;
}

if (visitingPackages.has(packageName)) {
return;
}

visitingPackages.add(packageName);

const packageJson = await readPackageJson(packageName);
const directDependencies = Object.keys(packageJson.dependencies || {})
.filter((depName) => ckeditorPackageSet.has(depName))
.sort();

for (const dependencyName of directDependencies) {
await visit(dependencyName);
}

visitingPackages.delete(packageName);
visitedPackages.add(packageName);
orderedPackages.push(packageName);
}

for (const packageName of ckeditorPackages.sort()) {
await visit(packageName);
}

const cssChunks = [];

for (const packageName of orderedPackages) {
const cssPath = path.join(cwd, 'node_modules', packageName, 'dist', 'index.css');

try {
let css = await fs.readFile(cssPath, 'utf8');
css = css.replace(/^\/\*# sourceMappingURL=.*?\*\/\s*$/gm, '');

if (css.trim().length === 0) {
continue;
}

cssChunks.push(`/* ${packageName} */\n${css.trim()}`);
} catch {
// Some CKEditor packages do not ship CSS assets.
}
}

const output = `/* Auto-generated by bin/build-ckeditor-styles.mjs. Do not edit manually. */\n\n${cssChunks.join('\n\n')}\n`;

await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, output, 'utf8');

console.log(`Generated CKEditor stylesheet: ${outputPath}`);
119 changes: 119 additions & 0 deletions bin/build-ckeditor-translations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/* eslint-env node */

import fs from 'fs/promises';
import path from 'path';
import { pathToFileURL } from 'url';

const core = process.env.OPENPROJECT_CORE;

if (!core) {
throw new Error("Expected OPENPROJECT_CORE to be present, but wasn't.");
}

const cwd = process.cwd();
const nodeModulesRoot = path.join(cwd, 'node_modules', '@ckeditor');
const outputDir = path.join(core, 'frontend', 'src', 'vendor', 'ckeditor', 'translations');

function serializePluralForm(fn) {
const source = fn.toString().trim();

if (source.includes('=>') || source.startsWith('function')) {
return source;
}

// Handle method syntax emitted by CKEditor translation bundles:
// "getPluralForm(n){ return ... }"
if (/^[A-Za-z_$][\w$]*\s*\(/.test(source)) {
return `function ${source}`;
}

return '(n) => n != 1';
}

/**
* @typedef {{ dictionary: Record<string, string | string[]>, getPluralForm?: Function }} LocalePayload
*/

/** @type {Map<string, LocalePayload>} */
const localeMap = new Map();

const entries = await fs.readdir(nodeModulesRoot, { withFileTypes: true });
const packageDirs = entries
.filter((entry) => entry.isDirectory() && entry.name.startsWith('ckeditor5-'))
.map((entry) => path.join(nodeModulesRoot, entry.name));

for (const packageDir of packageDirs) {
const translationsDir = path.join(packageDir, 'dist', 'translations');
let files;

try {
files = await fs.readdir(translationsDir);
} catch {
continue;
}

for (const fileName of files) {
if (!fileName.endsWith('.js') || fileName.endsWith('.umd.js')) {
continue;
}

const modulePath = path.join(translationsDir, fileName);
const module = await import(pathToFileURL(modulePath).href);
const data = module.default;

if (!data || typeof data !== 'object') {
continue;
}

for (const [locale, payload] of Object.entries(data)) {
if (!payload || typeof payload !== 'object') {
continue;
}

const existing = localeMap.get(locale) || { dictionary: {} };
const dictionary = payload.dictionary || {};

existing.dictionary = {
...existing.dictionary,
...dictionary,
};

if (typeof payload.getPluralForm === 'function') {
existing.getPluralForm = payload.getPluralForm;
}

localeMap.set(locale, existing);
}
}
}

await fs.mkdir(outputDir, { recursive: true });

for (const [locale, payload] of localeMap.entries()) {
const getPluralForm = payload.getPluralForm
? serializePluralForm(payload.getPluralForm)
: '(n) => n != 1';
const dictionary = JSON.stringify(payload.dictionary);

const content = `/**
* Auto-generated by bin/build-ckeditor-translations.mjs
* Do not edit manually.
*/
const locale = ${JSON.stringify(locale)};
const dictionary = ${dictionary};
const getPluralForm = ${getPluralForm};

window.CKEDITOR_TRANSLATIONS ||= {};
window.CKEDITOR_TRANSLATIONS[locale] ||= { dictionary: {}, getPluralForm };
window.CKEDITOR_TRANSLATIONS[locale].dictionary = {
...window.CKEDITOR_TRANSLATIONS[locale].dictionary,
...dictionary
};
window.CKEDITOR_TRANSLATIONS[locale].getPluralForm = getPluralForm;
`;

await fs.writeFile(path.join(outputDir, `${locale}.js`), content, 'utf8');
}

console.log(`Generated CKEditor translations: ${localeMap.size} locales -> ${outputDir}`);
12 changes: 12 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@ export default [
"no-undef": "error"
}
},
{
files: ["bin/**/*.mjs"],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
"no-unused-vars": "error",
"no-undef": "error"
}
},
];
Loading
Loading