summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json1
-rw-r--r--dev/bin/build.js64
-rw-r--r--dev/bin/dictionary-validate.js1
-rw-r--r--dev/bin/schema-validate.js5
-rw-r--r--dev/build-libs.js11
-rw-r--r--dev/dictionary-validate.js39
-rw-r--r--dev/generate-css-json.js61
-rw-r--r--dev/jsconfig.json7
-rw-r--r--dev/lint/global-declarations.js157
-rw-r--r--dev/lint/html-scripts.js202
-rw-r--r--dev/manifest-util.js84
-rw-r--r--dev/schema-validate.js25
-rw-r--r--dev/util.js21
-rw-r--r--vitest.config.js1
14 files changed, 269 insertions, 410 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 640a67e2..58262714 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -765,7 +765,6 @@
},
{
"files": [
- "dev/**/*.js",
"test/**/*.js"
],
"rules": {
diff --git a/dev/bin/build.js b/dev/bin/build.js
index 282f0414..c5814dd3 100644
--- a/dev/bin/build.js
+++ b/dev/bin/build.js
@@ -21,13 +21,22 @@ import childProcess from 'child_process';
import fs from 'fs';
import path from 'path';
import readline from 'readline';
-import {fileURLToPath} from 'url';
+import JSZip from 'jszip';
+import {fileURLToPath} from 'node:url';
import {buildLibs} from '../build-libs.js';
import {ManifestUtil} from '../manifest-util.js';
import {getAllFiles, getArgs, testMain} from '../util.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
+/**
+ * @param {string} directory
+ * @param {string[]} excludeFiles
+ * @param {string} outputFileName
+ * @param {string[]} sevenZipExes
+ * @param {?import('jszip').OnUpdateCallback} onUpdate
+ * @param {boolean} dryRun
+ */
async function createZip(directory, excludeFiles, outputFileName, sevenZipExes, onUpdate, dryRun) {
try {
fs.unlinkSync(outputFileName);
@@ -57,11 +66,17 @@ async function createZip(directory, excludeFiles, outputFileName, sevenZipExes,
}
}
}
- return await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun);
+ await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun);
}
+/**
+ * @param {string} directory
+ * @param {string[]} excludeFiles
+ * @param {string} outputFileName
+ * @param {?import('jszip').OnUpdateCallback} onUpdate
+ * @param {boolean} dryRun
+ */
async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun) {
- const JSZip = null;
const files = getAllFiles(directory);
removeItemsFromArray(files, excludeFiles);
const zip = new JSZip();
@@ -89,6 +104,10 @@ async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dr
}
}
+/**
+ * @param {string[]} array
+ * @param {string[]} removeItems
+ */
function removeItemsFromArray(array, removeItems) {
for (const item of removeItems) {
const index = getIndexOfFilePath(array, item);
@@ -98,6 +117,11 @@ function removeItemsFromArray(array, removeItems) {
}
}
+/**
+ * @param {string[]} array
+ * @param {string} item
+ * @returns {number}
+ */
function getIndexOfFilePath(array, item) {
const pattern = /\\/g;
const separator = '/';
@@ -110,6 +134,16 @@ function getIndexOfFilePath(array, item) {
return -1;
}
+/**
+ * @param {string} buildDir
+ * @param {string} extDir
+ * @param {ManifestUtil} manifestUtil
+ * @param {string[]} variantNames
+ * @param {string} manifestPath
+ * @param {boolean} dryRun
+ * @param {boolean} dryRunBuildZip
+ * @param {string} yomitanVersion
+ */
async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion) {
const sevenZipExes = ['7za', '7z'];
@@ -119,6 +153,7 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath,
}
const dontLogOnUpdate = !process.stdout.isTTY;
+ /** @type {import('jszip').OnUpdateCallback} */
const onUpdate = (metadata) => {
if (dontLogOnUpdate) { return; }
@@ -127,7 +162,7 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath,
message += ` (${metadata.currentFile})`;
}
- readline.clearLine(process.stdout);
+ readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(message);
};
@@ -173,6 +208,10 @@ async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath,
}
}
+/**
+ * @param {string} directory
+ * @param {string[]} files
+ */
function ensureFilesExist(directory, files) {
for (const file of files) {
assert.ok(fs.existsSync(path.join(directory, file)));
@@ -180,8 +219,11 @@ function ensureFilesExist(directory, files) {
}
+/**
+ * @param {string[]} argv
+ */
export async function main(argv) {
- const args = getArgs(argv, new Map([
+ const args = getArgs(argv, new Map(/** @type {[key: string, value: (boolean|null|number|string|string[])][]} */ ([
['all', false],
['default', false],
['manifest', null],
@@ -189,11 +231,11 @@ export async function main(argv) {
['dry-run-build-zip', false],
['yomitan-version', '0.0.0.0'],
[null, []]
- ]));
+ ])));
- const dryRun = args.get('dry-run');
- const dryRunBuildZip = args.get('dry-run-build-zip');
- const yomitanVersion = args.get('yomitan-version');
+ const dryRun = /** @type {boolean} */ (args.get('dry-run'));
+ const dryRunBuildZip = /** @type {boolean} */ (args.get('dry-run-build-zip'));
+ const yomitanVersion = /** @type {string} */ (args.get('yomitan-version'));
const manifestUtil = new ManifestUtil();
@@ -204,11 +246,11 @@ export async function main(argv) {
try {
await buildLibs();
- const variantNames = (
+ const variantNames = /** @type {string[]} */ ((
argv.length === 0 || args.get('all') ?
manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) :
args.get(null)
- );
+ ));
await build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion);
} finally {
// Restore manifest
diff --git a/dev/bin/dictionary-validate.js b/dev/bin/dictionary-validate.js
index 78ad5198..0affb919 100644
--- a/dev/bin/dictionary-validate.js
+++ b/dev/bin/dictionary-validate.js
@@ -28,6 +28,7 @@ async function main() {
return;
}
+ /** @type {import('dev/schema-validate').ValidateMode} */
let mode = null;
if (dictionaryFileNames[0] === '--ajv') {
mode = 'ajv';
diff --git a/dev/bin/schema-validate.js b/dev/bin/schema-validate.js
index 86cfebae..319c0d2c 100644
--- a/dev/bin/schema-validate.js
+++ b/dev/bin/schema-validate.js
@@ -17,8 +17,8 @@
*/
import fs from 'fs';
-import performance from 'perf_hooks';
-import {createJsonSchema} from '../util.js';
+import {performance} from 'perf_hooks';
+import {createJsonSchema} from '../schema-validate.js';
function main() {
const args = process.argv.slice(2);
@@ -30,6 +30,7 @@ function main() {
return;
}
+ /** @type {import('dev/schema-validate').ValidateMode} */
let mode = null;
if (args[0] === '--ajv') {
mode = 'ajv';
diff --git a/dev/build-libs.js b/dev/build-libs.js
index d33c1420..eee007f6 100644
--- a/dev/build-libs.js
+++ b/dev/build-libs.js
@@ -26,15 +26,18 @@ import {fileURLToPath} from 'url';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const extDir = path.join(dirname, '..', 'ext');
-async function buildLib(p) {
+/**
+ * @param {string} scriptPath
+ */
+async function buildLib(scriptPath) {
await esbuild.build({
- entryPoints: [p],
+ entryPoints: [scriptPath],
bundle: true,
minify: false,
sourcemap: true,
target: 'es2020',
format: 'esm',
- outfile: path.join(extDir, 'lib', path.basename(p)),
+ outfile: path.join(extDir, 'lib', path.basename(scriptPath)),
external: ['fs'],
banner: {
js: '// @ts-nocheck'
@@ -55,7 +58,7 @@ export async function buildLibs() {
const schemaDir = path.join(extDir, 'data/schemas/');
const schemaFileNames = fs.readdirSync(schemaDir);
- const schemas = schemaFileNames.map((schemaFileName) => JSON.parse(fs.readFileSync(path.join(schemaDir, schemaFileName))));
+ const schemas = schemaFileNames.map((schemaFileName) => JSON.parse(fs.readFileSync(path.join(schemaDir, schemaFileName), {encoding: 'utf8'})));
const ajv = new Ajv({schemas: schemas, code: {source: true, esm: true}});
const moduleCode = standaloneCode(ajv);
diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js
index eb40beda..b3654e75 100644
--- a/dev/dictionary-validate.js
+++ b/dev/dictionary-validate.js
@@ -22,23 +22,34 @@ import path from 'path';
import {performance} from 'perf_hooks';
import {createJsonSchema} from './schema-validate.js';
+/**
+ * @param {string} relativeFileName
+ * @returns {import('dev/dictionary-validate').Schema}
+ */
function readSchema(relativeFileName) {
const fileName = path.join(__dirname, relativeFileName);
const source = fs.readFileSync(fileName, {encoding: 'utf8'});
return JSON.parse(source);
}
+/**
+ * @param {import('dev/schema-validate').ValidateMode} mode
+ * @param {import('jszip')} zip
+ * @param {string} fileNameFormat
+ * @param {import('dev/dictionary-validate').Schema} schema
+ */
async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) {
let jsonSchema;
try {
jsonSchema = createJsonSchema(mode, schema);
} catch (e) {
- e.message += `\n(in file ${fileNameFormat})}`;
- throw e;
+ const e2 = e instanceof Error ? e : new Error(`${e}`);
+ e2.message += `\n(in file ${fileNameFormat})}`;
+ throw e2;
}
let index = 1;
while (true) {
- const fileName = fileNameFormat.replace(/\?/, index);
+ const fileName = fileNameFormat.replace(/\?/, `${index}`);
const file = zip.files[fileName];
if (!file) { break; }
@@ -47,14 +58,20 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) {
try {
jsonSchema.validate(data);
} catch (e) {
- e.message += `\n(in file ${fileName})}`;
- throw e;
+ const e2 = e instanceof Error ? e : new Error(`${e}`);
+ e2.message += `\n(in file ${fileName})}`;
+ throw e2;
}
++index;
}
}
+/**
+ * @param {import('dev/schema-validate').ValidateMode} mode
+ * @param {import('jszip')} archive
+ * @param {import('dev/dictionary-validate').Schemas} schemas
+ */
export async function validateDictionary(mode, archive, schemas) {
const fileName = 'index.json';
const indexFile = archive.files[fileName];
@@ -69,8 +86,9 @@ export async function validateDictionary(mode, archive, schemas) {
const jsonSchema = createJsonSchema(mode, schemas.index);
jsonSchema.validate(index);
} catch (e) {
- e.message += `\n(in file ${fileName})}`;
- throw e;
+ const e2 = e instanceof Error ? e : new Error(`${e}`);
+ e2.message += `\n(in file ${fileName})}`;
+ throw e2;
}
await validateDictionaryBanks(mode, archive, 'term_bank_?.json', version === 1 ? schemas.termBankV1 : schemas.termBankV3);
@@ -80,6 +98,9 @@ export async function validateDictionary(mode, archive, schemas) {
await validateDictionaryBanks(mode, archive, 'tag_bank_?.json', schemas.tagBankV3);
}
+/**
+ * @returns {import('dev/dictionary-validate').Schemas}
+ */
export function getSchemas() {
return {
index: readSchema('../ext/data/schemas/dictionary-index-schema.json'),
@@ -93,6 +114,10 @@ export function getSchemas() {
};
}
+/**
+ * @param {import('dev/schema-validate').ValidateMode} mode
+ * @param {string[]} dictionaryFileNames
+ */
export async function testDictionaryFiles(mode, dictionaryFileNames) {
const schemas = getSchemas();
diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js
index 914c1452..02e54530 100644
--- a/dev/generate-css-json.js
+++ b/dev/generate-css-json.js
@@ -16,9 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+import css from 'css';
import fs from 'fs';
import path from 'path';
+/**
+ * @returns {{cssFile: string, overridesCssFile: string, outputPath: string}[]}
+ */
export function getTargets() {
return [
{
@@ -34,8 +38,11 @@ export function getTargets() {
];
}
-import css from 'css';
-
+/**
+ * @param {import('css-style-applier').RawStyleData} rules
+ * @param {string[]} selectors
+ * @returns {number}
+ */
function indexOfRule(rules, selectors) {
const jj = selectors.length;
for (let i = 0, ii = rules.length; i < ii; ++i) {
@@ -53,6 +60,12 @@ function indexOfRule(rules, selectors) {
return -1;
}
+/**
+ * @param {import('css-style-applier').RawStyleDataStyleArray} styles
+ * @param {string} property
+ * @param {Map<string, number>} removedProperties
+ * @returns {number}
+ */
function removeProperty(styles, property, removedProperties) {
let removeCount = removedProperties.get(property);
if (typeof removeCount !== 'undefined') { return removeCount; }
@@ -69,6 +82,10 @@ function removeProperty(styles, property, removedProperties) {
return removeCount;
}
+/**
+ * @param {import('css-style-applier').RawStyleData} rules
+ * @returns {string}
+ */
export function formatRulesJson(rules) {
// Manually format JSON, for improved compactness
// return JSON.stringify(rules, null, 4);
@@ -102,27 +119,39 @@ export function formatRulesJson(rules) {
return result;
}
+/**
+ * @param {string} cssFile
+ * @param {string} overridesCssFile
+ * @returns {import('css-style-applier').RawStyleData}
+ * @throws {Error}
+ */
export function generateRules(cssFile, overridesCssFile) {
const content1 = fs.readFileSync(cssFile, {encoding: 'utf8'});
const content2 = fs.readFileSync(overridesCssFile, {encoding: 'utf8'});
- const stylesheet1 = css.parse(content1, {}).stylesheet;
- const stylesheet2 = css.parse(content2, {}).stylesheet;
+ const stylesheet1 = /** @type {css.StyleRules} */ (css.parse(content1, {}).stylesheet);
+ const stylesheet2 = /** @type {css.StyleRules} */ (css.parse(content2, {}).stylesheet);
const removePropertyPattern = /^remove-property\s+([\w\W]+)$/;
const removeRulePattern = /^remove-rule$/;
const propertySeparator = /\s+/;
+ /** @type {import('css-style-applier').RawStyleData} */
const rules = [];
// Default stylesheet
for (const rule of stylesheet1.rules) {
if (rule.type !== 'rule') { continue; }
- const {selectors, declarations} = rule;
+ const {selectors, declarations} = /** @type {css.Rule} */ (rule);
+ if (typeof selectors === 'undefined') { continue; }
+ /** @type {import('css-style-applier').RawStyleDataStyleArray} */
const styles = [];
- for (const declaration of declarations) {
- if (declaration.type !== 'declaration') { console.log(declaration); continue; }
- const {property, value} = declaration;
- styles.push([property, value]);
+ if (typeof declarations !== 'undefined') {
+ for (const declaration of declarations) {
+ if (declaration.type !== 'declaration') { console.log(declaration); continue; }
+ const {property, value} = /** @type {css.Declaration} */ (declaration);
+ if (typeof property !== 'string' || typeof value !== 'string') { continue; }
+ styles.push([property, value]);
+ }
}
if (styles.length > 0) {
rules.push({selectors, styles});
@@ -132,7 +161,9 @@ export function generateRules(cssFile, overridesCssFile) {
// Overrides
for (const rule of stylesheet2.rules) {
if (rule.type !== 'rule') { continue; }
- const {selectors, declarations} = rule;
+ const {selectors, declarations} = /** @type {css.Rule} */ (rule);
+ if (typeof selectors === 'undefined' || typeof declarations === 'undefined') { continue; }
+ /** @type {Map<string, number>} */
const removedProperties = new Map();
for (const declaration of declarations) {
switch (declaration.type) {
@@ -146,16 +177,18 @@ export function generateRules(cssFile, overridesCssFile) {
entry = {selectors, styles: []};
rules.push(entry);
}
- const {property, value} = declaration;
- removeProperty(entry.styles, property, removedProperties);
- entry.styles.push([property, value]);
+ const {property, value} = /** @type {css.Declaration} */ (declaration);
+ if (typeof property === 'string' && typeof value === 'string') {
+ removeProperty(entry.styles, property, removedProperties);
+ entry.styles.push([property, value]);
+ }
}
break;
case 'comment':
{
const index = indexOfRule(rules, selectors);
if (index < 0) { throw new Error('Could not find rule with matching selectors'); }
- const comment = declaration.comment.trim();
+ const comment = (/** @type {css.Comment} */ (declaration).comment || '').trim();
let m;
if ((m = removePropertyPattern.exec(comment)) !== null) {
for (const property of m[1].split(propertySeparator)) {
diff --git a/dev/jsconfig.json b/dev/jsconfig.json
index 5b1c450c..e0074980 100644
--- a/dev/jsconfig.json
+++ b/dev/jsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "module": "ES2015",
+ "module": "ES2022",
"target": "ES2022",
"checkJs": true,
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
@@ -73,6 +73,7 @@
"../types/other/globals.d.ts"
],
"exclude": [
- "../node_modules"
+ "../node_modules",
+ "lib"
]
} \ No newline at end of file
diff --git a/dev/lint/global-declarations.js b/dev/lint/global-declarations.js
deleted file mode 100644
index 648ad368..00000000
--- a/dev/lint/global-declarations.js
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2023 Yomitan Authors
- * Copyright (C) 2020-2022 Yomichan Authors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-const fs = require('fs');
-const path = require('path');
-const assert = require('assert');
-const {getAllFiles} = require('../util');
-
-
-/**
- * @param {string} string
- * @returns {string}
- */
-function escapeRegExp(string) {
- return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
-}
-
-/**
- * @param {string} string
- * @param {RegExp} pattern
- * @returns {number}
- */
-function countOccurences(string, pattern) {
- return (string.match(pattern) || []).length;
-}
-
-/**
- * @param {string} string
- * @returns {'\r'|'\n'|'\r\n'}
- */
-function getNewline(string) {
- const count1 = countOccurences(string, /(?:^|[^\r])\n/g);
- const count2 = countOccurences(string, /\r\n/g);
- const count3 = countOccurences(string, /\r(?:[^\n]|$)/g);
- if (count2 > count1) {
- return (count3 > count2) ? '\r' : '\r\n';
- } else {
- return (count3 > count1) ? '\r' : '\n';
- }
-}
-
-/**
- * @param {string} string
- * @param {string} substring
- * @returns {number}
- */
-function getSubstringCount(string, substring) {
- let count = 0;
- const pattern = new RegExp(`\\b${escapeRegExp(substring)}\\b`, 'g');
- while (true) {
- const match = pattern.exec(string);
- if (match === null) { break; }
- ++count;
- }
- return count;
-}
-
-
-/**
- * @param {string} fileName
- * @param {boolean} fix
- * @returns {boolean}
- */
-function validateGlobals(fileName, fix) {
- const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g;
- const trimPattern = /^[\s,*]+|[\s,*]+$/g;
- const splitPattern = /[\s,*]+/;
- const source = fs.readFileSync(fileName, {encoding: 'utf8'});
- let match;
- let first = true;
- let endIndex = 0;
- let newSource = '';
- const allGlobals = [];
- const newline = getNewline(source);
- while ((match = pattern.exec(source)) !== null) {
- if (!first) {
- console.error(`Encountered more than one global declaration in ${fileName}`);
- return false;
- }
- first = false;
-
- const parts = match[1].replace(trimPattern, '').split(splitPattern);
- parts.sort();
-
- const actual = match[0];
- const expected = `/* global${parts.map((v) => `${newline} * ${v}`).join('')}${newline} */`;
-
- try {
- assert.strictEqual(actual, expected);
- } catch (e) {
- console.error(`Global declaration error encountered in ${fileName}:`);
- console.error(e instanceof Error ? e.message : `${e}`);
- if (!fix) {
- return false;
- }
- }
-
- newSource += source.substring(0, match.index);
- newSource += expected;
- endIndex = match.index + match[0].length;
-
- allGlobals.push(...parts);
- }
-
- newSource += source.substring(endIndex);
-
- // This is an approximate check to see if a global variable is unused.
- // If the global appears in a comment, string, or similar, the check will pass.
- let errorCount = 0;
- for (const global of allGlobals) {
- if (getSubstringCount(newSource, global) <= 1) {
- console.error(`Global variable ${global} appears to be unused in ${fileName}`);
- ++errorCount;
- }
- }
-
- if (fix) {
- fs.writeFileSync(fileName, newSource, {encoding: 'utf8'});
- }
-
- return errorCount === 0;
-}
-
-
-/** */
-function main() {
- const fix = (process.argv.length >= 2 && process.argv[2] === '--fix');
- const directory = path.resolve(__dirname, '..', '..', 'ext');
- const pattern = /\.js$/;
- const ignorePattern = /^lib[\\/]/;
- const fileNames = getAllFiles(directory, (f) => pattern.test(f) && !ignorePattern.test(f));
- for (const fileName of fileNames) {
- if (!validateGlobals(path.join(directory, fileName), fix)) {
- process.exit(-1);
- return;
- }
- }
- process.exit(0);
-}
-
-
-if (require.main === module) { main(); }
diff --git a/dev/lint/html-scripts.js b/dev/lint/html-scripts.js
deleted file mode 100644
index da8c2c71..00000000
--- a/dev/lint/html-scripts.js
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2023 Yomitan Authors
- * Copyright (C) 2020-2022 Yomichan Authors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-const fs = require('fs');
-const path = require('path');
-const assert = require('assert');
-const {JSDOM} = require('jsdom');
-const {getAllFiles} = require('../util');
-
-
-/**
- * @param {string} fileName
- * @returns {?fs.Stats}
- */
-function lstatSyncSafe(fileName) {
- try {
- return fs.lstatSync(fileName);
- } catch (e) {
- return null;
- }
-}
-
-/**
- * @param {string} src
- * @param {string} fileName
- * @param {string} extDir
- */
-function validatePath(src, fileName, extDir) {
- assert.ok(typeof src === 'string', `<script> missing src attribute in ${fileName}`);
- assert.ok(src.startsWith('/'), `<script> src attribute is not absolute in ${fileName} (src=${JSON.stringify(src)})`);
- const relativeSrc = src.substring(1);
- assert.ok(!path.isAbsolute(relativeSrc), `<script> src attribute is invalid in ${fileName} (src=${JSON.stringify(src)})`);
- const fullSrc = path.join(extDir, relativeSrc);
- const stats = lstatSyncSafe(fullSrc);
- assert.ok(stats !== null, `<script> src file not found in ${fileName} (src=${JSON.stringify(src)})`);
- assert.ok(stats.isFile(), `<script> src file invalid in ${fileName} (src=${JSON.stringify(src)})`);
-}
-
-/**
- * @param {string} string
- * @param {RegExp} pattern
- * @returns {number}
- */
-function getSubstringCount(string, pattern) {
- let count = 0;
- while (true) {
- const match = pattern.exec(string);
- if (match === null) { break; }
- ++count;
- }
- return count;
-}
-
-/**
- * @param {string[]} scriptPaths
- * @returns {string[]}
- */
-function getSortedScriptPaths(scriptPaths) {
- // Sort file names without the extension
- const extensionPattern = /\.[^.]*$/;
- const scriptPaths2 = scriptPaths.map((value) => {
- const match = extensionPattern.exec(value);
- let ext = '';
- if (match !== null) {
- ext = match[0];
- value = value.substring(0, value.length - ext.length);
- }
- return {value, ext};
- });
-
- const stringComparer = new Intl.Collator('en-US'); // Invariant locale
- scriptPaths2.sort((a, b) => stringComparer.compare(a.value, b.value));
-
- return scriptPaths2.map(({value, ext}) => `${value}${ext}`);
-}
-
-/**
- * @param {string} fileName
- * @param {import('jsdom').DOMWindow} window
- * @throws {Error}
- */
-function validateScriptOrder(fileName, window) {
- const {document, Node: {ELEMENT_NODE, TEXT_NODE}, NodeFilter} = window;
-
- const scriptElements = document.querySelectorAll('script');
- if (scriptElements.length === 0) { return; }
-
- // Assert all scripts are siblings
- const scriptContainerElement = /** @type {Node} */ (scriptElements[0].parentNode);
- for (const element of scriptElements) {
- if (element.parentNode !== scriptContainerElement) {
- assert.fail('All script nodes are not contained within the same element');
- }
- }
-
- // Get script groupings and order
- /** @type {string[][]} */
- const scriptGroups = [];
- const newlinePattern = /\n/g;
- let separatingText = '';
- const walker = document.createTreeWalker(scriptContainerElement, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
- walker.firstChild();
- for (let node = /** @type {?Node} */ (walker.currentNode); node !== null; node = walker.nextSibling()) {
- switch (node.nodeType) {
- case ELEMENT_NODE:
- if (/** @type {Element} */ (node).tagName.toLowerCase() === 'script') {
- /** @type {string[]} */
- let scriptGroup;
- if (scriptGroups.length === 0 || getSubstringCount(separatingText, newlinePattern) >= 2) {
- scriptGroup = [];
- scriptGroups.push(scriptGroup);
- } else {
- scriptGroup = scriptGroups[scriptGroups.length - 1];
- }
- scriptGroup.push(/** @type {HTMLScriptElement} */ (node).src);
- separatingText = '';
- }
- break;
- case TEXT_NODE:
- separatingText += node.nodeValue;
- break;
- }
- }
-
- // Ensure core.js is first (if it is present)
- const ignorePattern = /^\/lib\//;
- const index = scriptGroups.flat()
- .filter((value) => !ignorePattern.test(value))
- .findIndex((value) => (value === '/js/core.js'));
- assert.ok(index <= 0, 'core.js is not the first included script');
-
- // Check script order
- for (let i = 0, ii = scriptGroups.length; i < ii; ++i) {
- const scriptGroup = scriptGroups[i];
- try {
- assert.deepStrictEqual(scriptGroup, getSortedScriptPaths(scriptGroup));
- } catch (e) {
- console.error(`Script order for group ${i + 1} in file ${fileName} is not correct:`);
- throw e;
- }
- }
-}
-
-/**
- * @param {string} fileName
- * @param {string} extDir
- */
-function validateHtmlScripts(fileName, extDir) {
- const fullFileName = path.join(extDir, fileName);
- const domSource = fs.readFileSync(fullFileName, {encoding: 'utf8'});
- const dom = new JSDOM(domSource);
- const {window} = dom;
- const {document} = window;
- try {
- for (const {src} of document.querySelectorAll('script')) {
- validatePath(src, fullFileName, extDir);
- }
- for (const {href} of document.querySelectorAll('link')) {
- validatePath(href, fullFileName, extDir);
- }
- validateScriptOrder(fileName, window);
- } finally {
- window.close();
- }
-}
-
-
-/** */
-function main() {
- try {
- const extDir = path.resolve(__dirname, '..', '..', 'ext');
- const pattern = /\.html$/;
- const ignorePattern = /^lib[\\/]/;
- const fileNames = getAllFiles(extDir, (f) => pattern.test(f) && !ignorePattern.test(f));
- for (const fileName of fileNames) {
- validateHtmlScripts(fileName, extDir);
- }
- } catch (e) {
- console.error(e);
- process.exit(-1);
- return;
- }
- process.exit(0);
-}
-
-
-if (require.main === module) { main(); }
diff --git a/dev/manifest-util.js b/dev/manifest-util.js
index 15175e7f..1efc8cfc 100644
--- a/dev/manifest-util.js
+++ b/dev/manifest-util.js
@@ -23,6 +23,11 @@ import path from 'path';
const dirname = path.dirname(fileURLToPath(import.meta.url));
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
@@ -31,16 +36,24 @@ function clone(value) {
export class ManifestUtil {
constructor() {
const fileName = path.join(dirname, 'data', 'manifest-variants.json');
- const {manifest, variants, defaultVariant} = JSON.parse(fs.readFileSync(fileName));
+ const {manifest, variants, defaultVariant} = /** @type {import('dev/manifest').Manifest} */ (JSON.parse(fs.readFileSync(fileName, {encoding: 'utf8'})));
+ /** @type {chrome.runtime.Manifest} */
this._manifest = manifest;
+ /** @type {import('dev/manifest').ManifestVariant[]} */
this._variants = variants;
+ /** @type {string} */
this._defaultVariant = defaultVariant;
+ /** @type {Map<string, import('dev/manifest').ManifestVariant>} */
this._variantMap = new Map();
for (const variant of variants) {
this._variantMap.set(variant.name, variant);
}
}
+ /**
+ * @param {?string} [variantName]
+ * @returns {chrome.runtime.Manifest}
+ */
getManifest(variantName) {
if (typeof variantName === 'string') {
const variant = this._variantMap.get(variantName);
@@ -59,20 +72,36 @@ export class ManifestUtil {
return clone(this._manifest);
}
+ /**
+ * @returns {import('dev/manifest').ManifestVariant[]}
+ */
getVariants() {
return [...this._variants];
}
+ /**
+ * @param {string} name
+ * @returns {import('dev/manifest').ManifestVariant|undefined}
+ */
getVariant(name) {
return this._variantMap.get(name);
}
+ /**
+ * @param {chrome.runtime.Manifest} manifest
+ * @returns {string}
+ */
static createManifestString(manifest) {
return JSON.stringify(manifest, null, 4) + '\n';
}
// Private
+ /**
+ * @param {import('dev/manifest').Command} data
+ * @returns {string}
+ * @throws {Error}
+ */
_evaluateModificationCommand(data) {
const {command, args, trim} = data;
const {stdout, stderr, status} = childProcess.spawnSync(command, args, {
@@ -89,6 +118,11 @@ export class ManifestUtil {
return result;
}
+ /**
+ * @param {chrome.runtime.Manifest} manifest
+ * @param {import('dev/manifest').Modification[]} modifications
+ * @returns {chrome.runtime.Manifest}
+ */
_applyModifications(manifest, modifications) {
if (Array.isArray(modifications)) {
for (const modification of modifications) {
@@ -97,6 +131,7 @@ export class ManifestUtil {
case 'set':
{
let {value, before, after, command} = modification;
+ /** @type {import('core').UnknownObject} */
const object = this._getObjectProperties(manifest, path2, path2.length - 1);
const key = path2[path2.length - 1];
@@ -121,6 +156,7 @@ export class ManifestUtil {
case 'replace':
{
const {pattern, patternFlags, replacement} = modification;
+ /** @type {import('core').UnknownObject} */
const value = this._getObjectProperties(manifest, path2, path2.length - 1);
const regex = new RegExp(pattern, patternFlags);
const last = path2[path2.length - 1];
@@ -131,6 +167,7 @@ export class ManifestUtil {
break;
case 'delete':
{
+ /** @type {import('core').UnknownObject} */
const value = this._getObjectProperties(manifest, path2, path2.length - 1);
const last = path2[path2.length - 1];
delete value[last];
@@ -139,6 +176,7 @@ export class ManifestUtil {
case 'remove':
{
const {item} = modification;
+ /** @type {unknown[]} */
const value = this._getObjectProperties(manifest, path2, path2.length);
const index = value.indexOf(item);
if (index >= 0) { value.splice(index, 1); }
@@ -147,6 +185,7 @@ export class ManifestUtil {
case 'splice':
{
const {start, deleteCount, items} = modification;
+ /** @type {unknown[]} */
const value = this._getObjectProperties(manifest, path2, path2.length);
const itemsNew = items.map((v) => clone(v));
value.splice(start, deleteCount, ...itemsNew);
@@ -158,7 +197,9 @@ export class ManifestUtil {
const {newPath, before, after} = modification;
const oldKey = path2[path2.length - 1];
const newKey = newPath[newPath.length - 1];
+ /** @type {import('core').UnknownObject} */
const oldObject = this._getObjectProperties(manifest, path2, path2.length - 1);
+ /** @type {import('core').UnknownObject} */
const newObject = this._getObjectProperties(manifest, newPath, newPath.length - 1);
const oldObjectIsNewObject = this._arraysAreSame(path2, newPath, -1);
const value = oldObject[oldKey];
@@ -184,6 +225,7 @@ export class ManifestUtil {
case 'add':
{
const {items} = modification;
+ /** @type {unknown[]} */
const value = this._getObjectProperties(manifest, path2, path2.length);
const itemsNew = items.map((v) => clone(v));
value.push(...itemsNew);
@@ -196,6 +238,13 @@ export class ManifestUtil {
return manifest;
}
+ /**
+ * @template T
+ * @param {T[]} array1
+ * @param {T[]} array2
+ * @param {number} lengthOffset
+ * @returns {boolean}
+ */
_arraysAreSame(array1, array2, lengthOffset) {
let ii = array1.length;
if (ii !== array2.length) { return false; }
@@ -206,10 +255,21 @@ export class ManifestUtil {
return true;
}
+ /**
+ * @param {import('core').UnknownObject} object
+ * @param {string|number} key
+ * @returns {number}
+ */
_getObjectKeyIndex(object, key) {
- return Object.keys(object).indexOf(key);
+ return Object.keys(object).indexOf(typeof key === 'string' ? key : `${key}`);
}
+ /**
+ * @param {import('core').UnknownObject} object
+ * @param {string|number} key
+ * @param {unknown} value
+ * @param {number} index
+ */
_setObjectKeyAtIndex(object, key, value, index) {
if (index < 0 || typeof key === 'number' || Object.prototype.hasOwnProperty.call(object, key)) {
object[key] = value;
@@ -229,13 +289,24 @@ export class ManifestUtil {
}
}
+ /**
+ * @template [TReturn=unknown]
+ * @param {unknown} object
+ * @param {import('dev/manifest').PropertyPath} path2
+ * @param {number} count
+ * @returns {TReturn}
+ */
_getObjectProperties(object, path2, count) {
for (let i = 0; i < count; ++i) {
- object = object[path2[i]];
+ object = /** @type {import('core').UnknownObject} */ (object)[path2[i]];
}
- return object;
+ return /** @type {TReturn} */ (object);
}
+ /**
+ * @param {import('dev/manifest').ManifestVariant} variant
+ * @returns {import('dev/manifest').ManifestVariant[]}
+ */
_getInheritanceChain(variant) {
const visited = new Set();
const inheritance = [];
@@ -256,6 +327,11 @@ export class ManifestUtil {
return inheritance;
}
+ /**
+ * @param {chrome.runtime.Manifest} manifest
+ * @param {import('dev/manifest').ManifestVariant} variant
+ * @returns {chrome.runtime.Manifest}
+ */
_createVariantManifest(manifest, variant) {
let modifiedManifest = clone(manifest);
for (const {modifications} of this._getInheritanceChain(variant)) {
diff --git a/dev/schema-validate.js b/dev/schema-validate.js
index fbd6b06a..a1fe8455 100644
--- a/dev/schema-validate.js
+++ b/dev/schema-validate.js
@@ -17,31 +17,48 @@
*/
import Ajv from 'ajv';
+import {readFileSync} from 'fs';
import {JsonSchema} from '../ext/js/data/json-schema.js';
+import {DataError} from './data-error.js';
class JsonSchemaAjv {
+ /**
+ * @param {import('dev/schema-validate').Schema} schema
+ */
constructor(schema) {
const ajv = new Ajv({
meta: false,
strictTuples: false,
allowUnionTypes: true
});
- ajv.addMetaSchema(require('ajv/dist/refs/json-schema-draft-07.json'));
- this._validate = ajv.compile(schema);
+ const metaSchemaPath = require.resolve('ajv/dist/refs/json-schema-draft-07.json');
+ const metaSchema = JSON.parse(readFileSync(metaSchemaPath, {encoding: 'utf8'}));
+ ajv.addMetaSchema(metaSchema);
+ /** @type {import('ajv').ValidateFunction} */
+ this._validate = ajv.compile(/** @type {import('ajv').Schema} */ (schema));
}
+ /**
+ * @param {unknown} data
+ * @throws {Error}
+ */
validate(data) {
if (this._validate(data)) { return; }
const {errors} = this._validate;
- const error = new Error('Schema validation failed');
+ const error = new DataError('Schema validation failed');
error.data = JSON.parse(JSON.stringify(errors));
throw error;
}
}
+/**
+ * @param {import('dev/schema-validate').ValidateMode} mode
+ * @param {import('dev/schema-validate').Schema} schema
+ * @returns {JsonSchema|JsonSchemaAjv}
+ */
export function createJsonSchema(mode, schema) {
switch (mode) {
case 'ajv': return new JsonSchemaAjv(schema);
- default: return new JsonSchema(schema);
+ default: return new JsonSchema(/** @type {import('json-schema').Schema} */ (schema));
}
}
diff --git a/dev/util.js b/dev/util.js
index cabc40aa..3299dec4 100644
--- a/dev/util.js
+++ b/dev/util.js
@@ -20,6 +20,11 @@ import fs from 'fs';
import JSZip from 'jszip';
import path from 'path';
+/**
+ * @param {string[]} args
+ * @param {Map<?string, (boolean|null|number|string|string[])>} argMap
+ * @returns {Map<?string, (boolean|null|number|string|string[])>}
+ */
export function getArgs(args, argMap) {
let key = null;
let canKey = true;
@@ -64,11 +69,16 @@ export function getArgs(args, argMap) {
return argMap;
}
+/**
+ * @param {string} baseDirectory
+ * @param {?(fileName: string) => boolean} predicate
+ * @returns {string[]}
+ */
export function getAllFiles(baseDirectory, predicate=null) {
const results = [];
const directories = [baseDirectory];
while (directories.length > 0) {
- const directory = directories.shift();
+ const directory = /** @type {string} */ (directories.shift());
const fileNames = fs.readdirSync(directory);
for (const fileName of fileNames) {
const fullFileName = path.join(directory, fileName);
@@ -86,6 +96,11 @@ export function getAllFiles(baseDirectory, predicate=null) {
return results;
}
+/**
+ * @param {string} dictionaryDirectory
+ * @param {string} [dictionaryName]
+ * @returns {import('jszip')}
+ */
export function createDictionaryArchive(dictionaryDirectory, dictionaryName) {
const fileNames = fs.readdirSync(dictionaryDirectory);
@@ -125,6 +140,10 @@ export function createDictionaryArchive(dictionaryDirectory, dictionaryName) {
// return zipFileBlob;
}
+/**
+ * @param {(...args: import('core').SafeAny[]) => (unknown|Promise<unknown>)} func
+ * @param {...import('core').SafeAny} args
+ */
export async function testMain(func, ...args) {
try {
await func(...args);
diff --git a/vitest.config.js b/vitest.config.js
index 9e1c54a5..3b6cdde0 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -24,6 +24,7 @@ export default defineConfig({
'test/playwright/**'
],
environment: 'jsdom',
+ // @ts-ignore - Appears to not be defined in the type definitions (https://vitest.dev/advanced/pool)
poolOptions: {
threads: {
useAtomics: true