Upgrade to ESLint 10 and import bundled JSON with import attributes

ESLint moves to a flat config (eslint.config.mjs) with the same rules,
run directly by npm run lint; @vue/cli-plugin-eslint is dropped since the
Vue build does not need it. With a parser that understands import
attributes, the bundled data files (brands, skills, gear, finalFest,
shiftyStations) are imported with `with { type: 'json' }`, which Node and
the Workers bundler both support, instead of being read through node:fs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Matt Isenhower
2026-09-05 09:59:09 -07:00
parent 19d1fb5d3d
commit ea3639ea99
9 changed files with 442 additions and 634 deletions

View File

@@ -1,48 +0,0 @@
/* eslint-env node */
// const createAliasSetting = require('@vue/eslint-config-airbnb/createAliasSetting');
module.exports = {
'root': true,
'extends': [
'plugin:vue/vue2-recommended',
'eslint:recommended',
],
'rules': {
// ESLint
// 'indent': ['warn', 2, { 'SwitchCase': 1 }],
'comma-dangle': ['warn', 'always-multiline'],
'no-unused-vars': ['warn', { 'args': 'none' }],
'semi': 'warn',
'quotes': ['warn' , 'single'],
'object-curly-spacing': ['warn', 'always'],
// Vue
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
'vue/max-attributes-per-line': ['warn', { singleline: { max: 4 } }],
'vue/html-self-closing': ['warn', { html: { void: 'always' } }],
'vue/no-deprecated-filter': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/no-deprecated-destroyed-lifecycle': 'off',
},
'globals': {
'__dirname': 'readonly',
'process': 'readonly',
'require': 'readonly',
'module': 'readonly',
'Buffer': 'readonly',
},
'env': {
'browser': true,
'es2022': true,
'node': true,
},
'ignorePatterns': [
'src/assets/i18n/index.mjs', // "assert" syntax is currently unrecognized
],
settings: {
// ...createAliasSetting({
// '@': './src',
// }),
},
};

46
eslint.config.mjs Normal file
View File

@@ -0,0 +1,46 @@
import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import globals from 'globals';
export default [
{
ignores: ['dist/**', 'node_modules/**', '.wrangler/**', 'workers/**/.wrangler/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/vue2-recommended'],
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
// ESLint
'comma-dangle': ['warn', 'always-multiline'],
'no-unused-vars': ['warn', { 'args': 'none' }],
'semi': 'warn',
'quotes': ['warn' , 'single'],
'object-curly-spacing': ['warn', 'always'],
// Vue
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
'vue/max-attributes-per-line': ['warn', { singleline: { max: 4 } }],
'vue/html-self-closing': ['warn', { html: { void: 'always' } }],
'vue/no-deprecated-filter': 'off',
'vue/require-toggle-inside-transition': 'off',
'vue/no-deprecated-destroyed-lifecycle': 'off',
},
},
// Root config files are CommonJS (the package has no "type": "module")
{
files: ['*.config.js'],
languageOptions: { sourceType: 'commonjs' },
},
];

945
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern --no-clean",
"test": "node --test && npm run worker:test",
"lint": "vue-cli-service lint",
"lint": "eslint .",
"cron": "node src/app/cron.js",
"start": "npm run sync:download && npm run splatnet && npm run twitter && npm run cron",
"locale-man": "node node_modules/locale-man/ -l en,es,es-MX,fr,fr-CA,de,nl,it,ru,ja -o src/locale",
@@ -57,12 +57,13 @@
},
"devDependencies": {
"@cloudflare/vitest-plugin": "^1.1.3",
"@eslint/js": "^10.0.1",
"@fullhuman/postcss-purgecss": "^8.0.0",
"@vue/cli-plugin-babel": "^5.0.9",
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"eslint": "^8.57.1",
"eslint": "^10.10.0",
"eslint-plugin-vue": "^10.10.0",
"globals": "^17.12.0",
"locale-man": "^0.0.5",
"sass": "^1.103.1",
"sass-loader": "^17.0.1",

View File

@@ -1,11 +1,10 @@
import TwitterPostBase from './TwitterPostBase.js';
import { captureScheduleScreenshot } from '../../screenshots/index.js';
import { readData, readJson, readJsonFile } from '../../../common/utilities.js';
import { readData, readJson } from '../../../common/utilities.js';
import finalFest from '../../../common/data/finalFest.json' with { type: 'json' };
import shiftyStations from '../../../common/data/shiftyStations.json' with { type: 'json' };
import path from 'node:path';
const finalFest = readJsonFile(new URL('../../../common/data/finalFest.json', import.meta.url));
const shiftyStations = readJsonFile(new URL('../../../common/data/shiftyStations.json', import.meta.url));
const stagesPath = path.resolve('storage/stages.json');
export default class ScheduleTweet extends TwitterPostBase {

View File

@@ -1,5 +1,5 @@
import Updater from './Updater.js';
import { readJsonFile } from '../../../common/utilities.js';
import skills from '../../../common/data/skills.json' with { type: 'json' };
export default class OriginalGearImageUpdater extends Updater {
constructor(storage) {
@@ -11,7 +11,7 @@ export default class OriginalGearImageUpdater extends Updater {
async update() {
// Get the list of skills
let data = readJsonFile(new URL('../../../common/data/skills.json', import.meta.url));
let data = skills;
// Retrieve skill images
await this.downloadImages(data);

View File

@@ -1,8 +1,6 @@
import { readJsonFile } from './utilities.js';
const brands = readJsonFile(new URL('./data/brands.json', import.meta.url));
const skills = readJsonFile(new URL('./data/skills.json', import.meta.url));
const inkipediaGear = readJsonFile(new URL('./data/gear.json', import.meta.url));
import brands from './data/brands.json' with { type: 'json' };
import skills from './data/skills.json' with { type: 'json' };
import inkipediaGear from './data/gear.json' with { type: 'json' };
export function getOriginalGear(gear) {
if (!gear || !gear.name)

View File

@@ -16,11 +16,6 @@ export function readJson(filename) {
return JSON.parse(fs.readFileSync(filename));
}
/** Read a JSON file relative to a module, e.g. readJsonFile(new URL('./data/x.json', import.meta.url)) */
export function readJsonFile(url) {
return JSON.parse(fs.readFileSync(url));
}
export function writeJson(filename, data) {
mkdirp(path.dirname(filename));
fs.writeFileSync(filename, JSON.stringify(data));

View File

@@ -7,7 +7,9 @@ import MerchandisesUpdater from '../../src/app/updater/updaters/MerchandisesUpda
import FestivalsUpdater from '../../src/app/updater/updaters/FestivalsUpdater.js';
import OriginalGearImageUpdater from '../../src/app/updater/updaters/OriginalGearImageUpdater.js';
import { createUpdaters } from '../../src/app/updater/updateAll.js';
import { getTopOfCurrentHour, readJsonFile } from '../../src/common/utilities.js';
import { getTopOfCurrentHour } from '../../src/common/utilities.js';
import gearData from '../../src/common/data/gear.json' with { type: 'json' };
import skills from '../../src/common/data/skills.json' with { type: 'json' };
import { fakeSplatNet, buckets, json, keys, setSessionEnvironment } from './support.js';
setSessionEnvironment();
@@ -79,7 +81,6 @@ test('timeline: keeps only coop and weapon availability, dropping hidden items',
});
test('merchandises: attaches original gear from the bundled data without its brand', async () => {
const gearData = readJsonFile(new URL('../../src/common/data/gear.json', import.meta.url));
const original = gearData.head[0];
const brand = { id: '0', name: 'Brand', image: '/images/brand/0.png', frequent_skill: { id: '0', name: 'Skill', image: '/images/skill/0.png' } };
const skill = { id: '0', name: 'Skill', image: '/images/skill/0.png' };
@@ -128,7 +129,6 @@ test('festivals: merges one region into the shared file, fetches missing ranking
});
test('original gear: mirrors skill images from the bundled skills data', async () => {
const skills = readJsonFile(new URL('../../src/common/data/skills.json', import.meta.url));
const images = Object.values(skills).map(s => s.image).filter(Boolean);
await b.publicBucket.put(`assets/splatnet${images[0]}`, 'existing');
const splatnet = fakeSplatNet();