mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-13 05:33:43 +01:00
* Remove double spaces * Enforce curly apostrophes * Enforce French typographic spaces in fr.json Add vitest test enforcing French spacing rules per https://typographisme.net/post/Les-espaces-typographiques-et-le-web: - NNBSP (U+202F) before ? ! ; - NBSP (U+00A0) before : - NBSP (U+00A0) around guillemets « » Fix 38 fr.json values that were using NBSP (or no space) before ? ! ;. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Extend French typography test to numbers and percent signs Add rules per https://typographisme.net/post/Les-espaces-typographiques-et-le-web: - NNBSP (U+202F) between digit groups (thousands separator) - NBSP (U+00A0) between a number and a following word/unit - NBSP (U+00A0) before % Fix 9 fr.json values where a regular space was used instead of NBSP between a number and a following word (e.g. "1 résultat", "10 jetons", "7 jours"). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use point médian for inclusive writing in fr.json Replace parenthetical gender/plural markers like "sûr(e)", "abonné(e)", "licence(s)" with the point médian "·" (e.g. "sûr·e", "abonné·e", "licence·s") per modern French écriture inclusive. Add a vitest rule rejecting "word(suffix)" patterns where suffix is 1-3 letters and there is no space before the opening parenthesis. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Simplify inclusive-writing check to "(e)" substring match The previous regex (matchAll without the /g flag) would have thrown at runtime. Drop matchAll entirely and use includes('(e)'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add point médian to participles in fr.json Apply inclusive writing (·e) to past participles agreeing with the addressed user ("affilié·e", "invité·e", "connecté·e", "abonné·e", etc.). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use inclusive forms in fr.json * Change "de Overleaf" to "d’Overleaf" * Fix tests * Check for consecutive spaces, not just double spaces * Update comment about guillemets * Add ESLint rules for locale JSON conventions Add three rules to @overleaf/eslint-plugin that run against locale JSON files via @eslint/json: - no-consecutive-spaces-in-locales - no-straight-apostrophes-in-locales - french-typography-in-locales Wire them up in services/web/eslint.config.mjs against `locales/*.json` (and `locales/fr.json` for the French-typography rule). * Add `@eslint/json` so JSON files can be parsed * Scope JS/TS-specific config to JS/TS files only so that built-in rules aren't applied to JSON files * Disable `no-irregular-whitespace` in french-typography-in-locales.js * Make the rules fixable * Improve regexes: user `\s` instead of custom matchers * Add sorted-keys-in-locales ESLint rule Mirrors scripts/translations/sort.js as a fixable ESLint rule: require top-level keys in locale JSON files to be sorted alphabetically. The autofix re-serializes the file with sorted keys. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add no-unused-locale-keys ESLint rule Mirrors scripts/translations/cleanupUnusedLocales.js as a fixable ESLint rule applied only to en.json. Shells out to `find` + `cat` once per process (cached) to grep all candidate source files for key references, then reports any en.json key that isn't found. The autofix re-serializes en.json without the unused keys. Hardcoded ALWAYS_USED set covers keys built at runtime (month names, third-party-datastore notifications) that the regex scan can't see. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add locale-variables-match-en ESLint rule Mirrors scripts/translations/checkVariables.js as an ESLint rule applied to non-en locale JSON files. Reads en.json (cached) and reports any member whose `__name__` placeholders differ from the base. GLOBALS (e.g. __appName__) are allowed as extras; IGNORE_NESTING_FOR preserves the script's per-key exceptions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Replace some double linebreak (@overleaf/no-consecutive-spaces-in-locales) * Add no-orphan-locale-keys ESLint rule Detect translation keys in non-en locale files that aren't present in en.json. Mirrors the `--sync-non-en` flag of cleanupUnusedLocales.js. The autofix re-serializes the locale file without the orphan keys, sorted alphabetically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Add tests for the locale JSON ESLint rules Cover the four single-file rules: - no-consecutive-spaces-in-locales - no-straight-apostrophes-in-locales - sorted-keys-in-locales - french-typography-in-locales (including the inclusive-writing suggestion for "(e)") The three cross-file rules (no-unused-locale-keys, no-orphan-locale-keys, locale-variables-match-en) read from disk and aren't covered here; they need integration tests via fixtures. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop bin/lint_locales — checks are now covered by ESLint rules The four checks in bin/lint_locales are duplicated by the new @overleaf/eslint-plugin rules: - sort.js --check → sorted-keys-in-locales - cleanupUnusedLocales.js --check → no-unused-locale-keys + no-orphan-locale-keys - checkVariables.js → locale-variables-match-en - single-quote grep → no-straight-apostrophes-in-locales `yarn lint` runs the ESLint rules against locales/*.json, so the lint_locales script and Makefile target are redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove Locales.test.mjs * Delete scripts replaced by @overleaf/eslint-plugin rules Remove the three locale scripts now superseded by ESLint rules: - scripts/translations/sort.js → sorted-keys-in-locales - scripts/translations/cleanupUnusedLocales.js → no-unused-locale-keys + no-orphan-locale-keys - scripts/translations/checkVariables.js → locale-variables-match-en Also drop the matching Makefile targets (sort_locales, cleanup_unused_locales), update AGENTS.md / copilot-instructions.md to point at `yarn lint --fix` instead, and prune the deleted entries from the esm-check-migration allowlist. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Delete redundant scripts/translations/sort-locale-file.js Single-file sort utility — superseded by `yarn lint --fix path/to/file.json` via the sorted-keys-in-locales ESLint rule. No callers in repo. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Update comments and messages * Add test file pattern to ESLint configuration * Reorder "Backend specific rules" so `no-extraneous-dependencies` doesn't fail on test files * Make french-typography fixes idempotent and catch edge cases Apply Copilot's review feedback on PR #34221: - autoFix() now uses `\\s*`/`\\s+` instead of `\\s?`/`\\s` so it collapses any run of mixed whitespace into the correct NBSP/NNBSP, making the fix idempotent and order-independent. - The `:` lint regex now captures optional whitespace and reports when it isn't NBSP (catches `"Liste :"` with a regular space). - The `%` lint regex now matches the missing-space case (`"10%"`) via `\\s*` instead of requiring one char between digit and `%`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove sort-locale-file.js from pre-commit * Revert no-unused-locale-keys ESLint rule; Restore lint_locales scripts --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> GitOrigin-RevId: 8351fab074e6e7f77920b70237df1cd1c781e053
301 lines
8.8 KiB
JavaScript
301 lines
8.8 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import minimist from 'minimist'
|
|
|
|
const APP_CODE_PATH = ['app', 'modules', 'scripts', 'test']
|
|
|
|
// These have already been converted but don't have a `.mjs` extension
|
|
const converted = new Set([
|
|
'scripts/ukamf/check-certs.js',
|
|
'scripts/ukamf/check-idp-metadata.js',
|
|
'scripts/ukamf/metadata-processor.js',
|
|
'scripts/ukamf/ukamf-db.js',
|
|
'scripts/ukamf/ukamf-entity.js',
|
|
'scripts/translations/checkCoverage.js',
|
|
'scripts/translations/checkSanitizeOptions.js',
|
|
'scripts/translations/checkVariables.js',
|
|
'scripts/translations/cleanupUnusedLocales.js',
|
|
'scripts/translations/config.js',
|
|
'scripts/translations/download.js',
|
|
'scripts/translations/insertHTMLFragments.js',
|
|
'scripts/translations/replaceLinkFragments.js',
|
|
'scripts/translations/sanitize.js',
|
|
'scripts/translations/transformLocales.js',
|
|
'scripts/translations/translateLocales.js',
|
|
'scripts/translations/upload.js',
|
|
'scripts/translations/uploadNonEnglish.js',
|
|
'scripts/translations/utils.js',
|
|
])
|
|
// These files are not to be converted (e.g. they use CommonJS features that are not available in ES Modules)
|
|
const excluded = new Set([
|
|
'modules/server-ce-scripts/scripts/create-user.js', // must be CJS for backwards compatibility
|
|
'test/acceptance/config/settings.test.saas.js', // must be CJS for @overleaf/settings module
|
|
'test/acceptance/config/settings.test.server-pro.js', // must be CJS for @overleaf/settings module
|
|
'app/src/infrastructure/PackageVersions.js', // required by webpack
|
|
])
|
|
|
|
function fileIsESM(file) {
|
|
const relativePath = file.replace(process.cwd() + '/', '')
|
|
return file.endsWith('.mjs') || converted.has(relativePath)
|
|
}
|
|
|
|
function fileCanBeConvertedToESM(file) {
|
|
const relativePath = file.replace(process.cwd() + '/', '')
|
|
if (fileIsESM(relativePath)) {
|
|
return false
|
|
}
|
|
return !excluded.has(relativePath)
|
|
}
|
|
|
|
const {
|
|
_: args,
|
|
files,
|
|
help,
|
|
json,
|
|
} = minimist(process.argv.slice(2), {
|
|
boolean: ['files', 'help', 'json'],
|
|
alias: {
|
|
files: 'f',
|
|
help: 'h',
|
|
json: 'j',
|
|
},
|
|
default: {
|
|
files: false,
|
|
help: false,
|
|
json: false,
|
|
},
|
|
})
|
|
const paths = args.length > 0 ? args : APP_CODE_PATH
|
|
|
|
function usage() {
|
|
console.error(`Usage: node check-esm-migration.js [OPTS...] dir1 dir2
|
|
node check-esm-migration.js file
|
|
|
|
Usage with directories
|
|
----------------------
|
|
|
|
When the arguments are a list of directories it prints the status of ES Modules migration within those directories.
|
|
When no directory is provided, it checks app/ and modules/, which represent the entire codebase.
|
|
|
|
With the --files (-f) option, it prints the list of JS files that:
|
|
- Are not migrated to ESM
|
|
- Are not required by any file that is not migrated yet to ESM (in the entire codebase)
|
|
|
|
These files should be the most immediate candidates to be migrated.
|
|
|
|
WARNING: please note that this script only looks up literals in require() statements, so paths
|
|
built dynamically (such as those in infrastructure/Modules.js) are not being taken into account.
|
|
|
|
Usage with a JS file
|
|
--------------------
|
|
|
|
When the argument is a JS file, the script outputs the files that depend on this file that have not been converted
|
|
yet to ES Modules.
|
|
|
|
The files in the list must to be converted to ES Modules before converting the JS file.
|
|
|
|
Example:
|
|
node scrips/check-esm-migration.js --files modules/admin-panel
|
|
node scrips/check-esm-migration.js app/src/router.js
|
|
|
|
Options:
|
|
--files Prints the files that are not imported by app code via CommonJS
|
|
--json Prints the result in JSON format, including the list of files from --files
|
|
--help Prints this help
|
|
`)
|
|
}
|
|
|
|
function resolveImportPaths(dir, file) {
|
|
const absolutePath = path.resolve(dir, file)
|
|
if (fs.existsSync(absolutePath)) {
|
|
return absolutePath
|
|
} else if (fs.existsSync(absolutePath + '.js')) {
|
|
return absolutePath + '.js'
|
|
} else if (fs.existsSync(absolutePath + '.mjs')) {
|
|
return absolutePath + '.mjs'
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function collectJsFiles(dir, files = []) {
|
|
const items = fs.readdirSync(dir)
|
|
items.forEach(item => {
|
|
const fullPath = path.join(dir, item)
|
|
const stat = fs.statSync(fullPath)
|
|
|
|
if (stat.isDirectory()) {
|
|
const basename = path.basename(fullPath)
|
|
|
|
// skipping directories from search
|
|
if (!['frontend', 'node_modules'].includes(basename)) {
|
|
collectJsFiles(fullPath, files)
|
|
}
|
|
} else if (
|
|
stat.isFile() &&
|
|
(fullPath.endsWith('.js') || fullPath.endsWith('.mjs'))
|
|
) {
|
|
files.push(fullPath)
|
|
}
|
|
})
|
|
return files
|
|
}
|
|
|
|
function extractImports(filePath) {
|
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
|
|
|
// not 100% compliant (string escaping, etc.) but does the work here
|
|
const contentWithoutComments = fileContent.replace(
|
|
/\/\/.*|\/\*[\s\S]*?\*\//g,
|
|
''
|
|
)
|
|
|
|
const requireRegex = /require\s*\(\s*['"](.+?)['"]\s*\)/g
|
|
|
|
const dependencies = []
|
|
while (true) {
|
|
const match = requireRegex.exec(contentWithoutComments)
|
|
if (!match) {
|
|
break
|
|
}
|
|
dependencies.push(match[1])
|
|
}
|
|
|
|
// build absolute path for the imported file
|
|
return dependencies
|
|
.map(depPath => resolveImportPaths(path.dirname(filePath), depPath))
|
|
.filter(path => path !== null)
|
|
}
|
|
|
|
// Main function to process a list of directories and create the Map of dependencies
|
|
function findJSAndImports(directories) {
|
|
const fileDependenciesMap = new Map()
|
|
|
|
directories.forEach(dir => {
|
|
if (fs.existsSync(dir)) {
|
|
const jsFiles = collectJsFiles(dir)
|
|
|
|
jsFiles.forEach(filePath => {
|
|
const imports = extractImports(filePath)
|
|
fileDependenciesMap.set(filePath, imports)
|
|
})
|
|
} else {
|
|
console.error(`Directory not found: ${dir}`)
|
|
process.exit(1)
|
|
}
|
|
})
|
|
|
|
return fileDependenciesMap
|
|
}
|
|
|
|
function printDirectoriesReport(allFilesAndImports) {
|
|
// collect all files that are imported via CommonJS in the entire backend codebase
|
|
const filesImportedViaCjs = new Set()
|
|
allFilesAndImports.forEach((imports, file) => {
|
|
if (!fileIsESM(file)) {
|
|
imports.forEach(imprt => filesImportedViaCjs.add(imprt))
|
|
}
|
|
})
|
|
|
|
// collect js files from the selected paths
|
|
const selectedFiles = Array.from(
|
|
findJSAndImports(paths.map(dir => path.resolve(dir))).keys()
|
|
).filter(file => !file.endsWith('settings.test.js'))
|
|
const nonMigratedFiles = selectedFiles.filter(fileCanBeConvertedToESM)
|
|
const migratedFileCount = selectedFiles.filter(fileIsESM).length
|
|
|
|
// collect files in the selected paths that are not imported via CommonJs in the entire backend codebase
|
|
const filesNotImportedViaCjs = nonMigratedFiles.filter(
|
|
file => !filesImportedViaCjs.has(file)
|
|
)
|
|
|
|
if (json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
fileCount: selectedFiles.length,
|
|
migratedFileCount,
|
|
filesNotImportedViaCjs,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
} else {
|
|
console.log(`Found ${selectedFiles.length} files in ${paths}:
|
|
- ${migratedFileCount} have been migrated to ES Modules (progress=${((migratedFileCount / selectedFiles.length) * 100).toFixed(2)}%)
|
|
- ${filesNotImportedViaCjs.length} are ready to migrate (these are not imported via CommonJS in the entire codebase)
|
|
`)
|
|
if (files) {
|
|
console.log(`Files that are ready to migrate:`)
|
|
filesNotImportedViaCjs.forEach(file =>
|
|
console.log(` - ${file.replace(process.cwd() + '/', '')}`)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function printFileReport(allFilesAndImports) {
|
|
const filePath = path.resolve(paths[0])
|
|
if (fileIsESM(filePath)) {
|
|
console.log(`${filePath} is already migrated to ESM`)
|
|
return
|
|
}
|
|
const filePathWithoutExtension = filePath.replace('.js', '')
|
|
|
|
const importingFiles = []
|
|
allFilesAndImports.forEach((imports, file) => {
|
|
if (fileIsESM(file)) {
|
|
return
|
|
}
|
|
if (
|
|
imports.some(
|
|
imprt => imprt === filePath || imprt === filePathWithoutExtension
|
|
)
|
|
) {
|
|
importingFiles.push(file)
|
|
}
|
|
})
|
|
|
|
if (json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
importingFiles,
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
} else {
|
|
console.log(`${filePath} is required by ${importingFiles.length} CJS file`)
|
|
importingFiles.forEach(file =>
|
|
console.log(` - ${file.replace(process.cwd() + '/', '')}`)
|
|
)
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
if (help) {
|
|
usage()
|
|
process.exit(0)
|
|
}
|
|
|
|
// collect all the js files in the entire backend codebase (app/ + modules/) with its imports
|
|
const allFilesAndImports = findJSAndImports(
|
|
APP_CODE_PATH.map(dir => path.resolve(dir))
|
|
)
|
|
const entryPoint = fs.existsSync('app.js') ? 'app.js' : 'app.mjs'
|
|
allFilesAndImports.set(path.resolve(entryPoint), extractImports(entryPoint))
|
|
|
|
const isFileReport = fs.statSync(paths[0]).isFile()
|
|
|
|
if (isFileReport) {
|
|
printFileReport(allFilesAndImports)
|
|
} else {
|
|
printDirectoriesReport(allFilesAndImports)
|
|
}
|
|
}
|
|
|
|
main()
|