Skip to content

Commit 6b315c3

Browse files
authored
Modernize and fix website build tooling deps and utilize JS type checking (#3348)
* Modernize and fix website build tooling deps and utilize JS type checking * Upgrade to the latest Node.js
1 parent 96d73a8 commit 6b315c3

18 files changed

+2041
-986
lines changed

.github/workflows/website.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ jobs:
2626
- name: 📥 Clone and checkout repository
2727
uses: actions/checkout@v3
2828

29+
# We can remove this step once `ubuntu-latest` has Node.js 22 or newer for its native TypeScript support. See:
30+
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
31+
# https://nodejs.org/en/learn/typescript/run-natively
32+
- name: 📦 Install the latest Node.js
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: "latest"
36+
2937
- name: 🕸 Install Zola
3038
uses: taiki-e/install-action@v2
3139
with:
@@ -77,7 +85,8 @@ jobs:
7785
MODE: prod
7886
run: |
7987
cd website
80-
npm run install-fonts
88+
npm ci
89+
npm run lint
8190
zola --config config.toml build --minify
8291
8392
- name: 📤 Publish to Cloudflare Pages

website/build-scripts/generate-editor-structure.js renamed to website/.build-scripts/generate-editor-structure.ts

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
1-
const fs = require("fs");
2-
const path = require("path");
3-
4-
/**
5-
* Escapes characters that have special meaning in HTML.
6-
* @param {string} text The text to escape.
7-
* @returns {string} The escaped text.
8-
*/
9-
function escapeHtml(text) {
10-
return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
11-
}
1+
/* eslint-disable no-console */
2+
3+
import fs from "fs";
4+
import path from "path";
125

13-
/**
14-
* Parses a single line of the input text.
15-
* @param {string} line The line to parse.
16-
* @returns {{ level: number, text: string, link: string | undefined }}
17-
*/
18-
function parseLine(line) {
6+
type Entry = { level: number; text: string; link: string | undefined };
7+
8+
/// Parses a single line of the input text.
9+
function parseLine(line: string) {
1910
const linkRegex = /`([^`]+)`$/;
2011
const linkMatch = line.match(linkRegex);
2112
let link = undefined;
@@ -25,22 +16,19 @@ function parseLine(line) {
2516
link = `https://github.com/GraphiteEditor/Graphite/blob/master/${filePath}`;
2617
}
2718

28-
const textContent = line.replace(/^[\s]*/, "").replace(linkRegex, "").trim();
19+
const textContent = line
20+
.replace(/^[\s]*/, "")
21+
.replace(linkRegex, "")
22+
.trim();
2923
const indentation = line.indexOf(textContent);
3024
// Each level of indentation is 4 characters.
3125
const level = Math.floor(indentation / 4);
3226

3327
return { level, text: textContent, link };
3428
}
3529

36-
/**
37-
* Recursively builds the HTML list from the parsed nodes.
38-
* @param {Array} nodes The array of parsed node objects.
39-
* @param {number} currentIndex The current index in the nodes array.
40-
* @param {number} currentLevel The current indentation level.
41-
* @returns {{html: string, nextIndex: number}}
42-
*/
43-
function buildHtmlList(nodes, currentIndex, currentLevel) {
30+
/// Recursively builds the HTML list from the parsed nodes.
31+
function buildHtmlList(nodes: Entry[], currentIndex: number, currentLevel: number) {
4432
if (currentIndex >= nodes.length) {
4533
return { html: "", nextIndex: currentIndex };
4634
}
@@ -68,16 +56,18 @@ function buildHtmlList(nodes, currentIndex, currentLevel) {
6856
} else {
6957
escapedText = [escapeHtml(node.text)];
7058
}
71-
59+
7260
let role = "message";
7361
if (node.link) role = "subsystem";
7462
else if (hasDeeperChildren) role = "submessage";
7563
else if (escapedText.length === 2) role = "field";
7664

7765
const partOfMessageFromNamingConvention = ["Message", "MessageHandler", "MessageContext"].some((suffix) => node.text.replace(/(.*)<.*>/g, "$1").endsWith(suffix));
7866
const partOfMessageViolatesNamingConvention = node.link && !partOfMessageFromNamingConvention;
79-
const violatesNamingConvention = partOfMessageViolatesNamingConvention ? "<span class=\"warn\">(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')</span>" : "";
80-
67+
const violatesNamingConvention = partOfMessageViolatesNamingConvention
68+
? "<span class=\"warn\">(violates naming convention — should end with 'Message', 'MessageHandler', or 'MessageContext')</span>"
69+
: "";
70+
8171
if (hasDirectChildren) {
8272
html += `<li><span class="tree-node"><span class="${role}">${escapedText}</span>${linkHtml}${violatesNamingConvention}</span>`;
8373
const childResult = buildHtmlList(nodes, i + 1, node.level + 1);
@@ -96,12 +86,16 @@ function buildHtmlList(nodes, currentIndex, currentLevel) {
9686
return { html, nextIndex: i };
9787
}
9888

89+
function escapeHtml(text: string) {
90+
return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
91+
}
92+
9993
const inputFile = process.argv[2];
10094
const outputFile = process.argv[3];
10195

10296
if (!inputFile || !outputFile) {
10397
console.error("Error: Please provide the input text and output HTML file paths as arguments.");
104-
console.log("Usage: node generate-editor-structure.js <input txt> <output html>");
98+
console.log("Usage: node generate-editor-structure.ts <input txt> <output html>");
10599
process.exit(1);
106100
}
107101

@@ -112,7 +106,7 @@ if (!fs.existsSync(inputFile)) {
112106

113107
try {
114108
const fileContent = fs.readFileSync(inputFile, "utf-8");
115-
const lines = fileContent.split(/\r?\n/).filter(line => line.trim() !== "" && !line.startsWith("// filepath:"));
109+
const lines = fileContent.split(/\r?\n/).filter((line) => line.trim() !== "" && !line.startsWith("// filepath:"));
116110
const parsedNodes = lines.map(parseLine);
117111

118112
const { html } = buildHtmlList(parsedNodes, 0, 0);

website/build-scripts/install-fonts.js renamed to website/.build-scripts/install-fonts.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
const fs = require("fs");
2-
const https = require("https");
3-
const path = require("path");
1+
/* eslint-disable no-console */
42

5-
// Define basePath
6-
const basePath = path.resolve(__dirname);
3+
import fs from "fs";
4+
import https from "https";
5+
import path from "path";
6+
7+
// Define basePath as the directory of the current script
8+
const basePath = import.meta.dirname;
79

810
// Define files to copy as [source, destination] pairs
911
// Files with the same destination will be concatenated
@@ -21,8 +23,8 @@ const DIRECTORIES_TO_COPY = [
2123

2224
// Track processed destination files and CSS content
2325
const processedDestinations = new Set();
24-
const cssDestinations = new Set();
25-
const allCopiedFiles = new Set();
26+
const cssDestinations = new Set<string>();
27+
const allCopiedFiles = new Set<string>();
2628

2729
// Process each file
2830
FILES_TO_COPY.forEach(([source, dest]) => {
@@ -68,8 +70,7 @@ FILES_TO_COPY.forEach(([source, dest]) => {
6870
}
6971
});
7072

71-
// Function to recursively copy a directory
72-
function copyDirectoryRecursive(source, destination) {
73+
function copyDirectoryRecursive(source: string, destination: string) {
7374
// Ensure destination directory exists
7475
if (!fs.existsSync(destination)) {
7576
fs.mkdirSync(destination, { recursive: true });
@@ -130,7 +131,7 @@ cssDestinations.forEach((cssPath) => {
130131
});
131132

132133
// Filter files that aren't referenced in CSS
133-
const unusedFiles = [];
134+
const unusedFiles: string[] = [];
134135
allCopiedFiles.forEach((filePath) => {
135136
const fileName = path.basename(filePath);
136137

@@ -185,10 +186,10 @@ https
185186
fs.writeFileSync(textBalancerDest, data, "utf8");
186187
console.log(`Downloaded and saved: ${textBalancerDest}`);
187188
} catch (error) {
188-
console.error(`Error saving text-balancer.js:`, error);
189+
console.error("Error saving text-balancer.js:", error);
189190
}
190191
});
191192
})
192193
.on("error", (err) => {
193-
console.error(`Error downloading text-balancer.js:`, err);
194+
console.error("Error downloading text-balancer.js:", err);
194195
});

website/.eslintrc.js

Lines changed: 0 additions & 86 deletions
This file was deleted.

website/eslint.config.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import js from "@eslint/js";
2+
import { defineConfig, globalIgnores } from "eslint/config";
3+
import * as pluginImport from "eslint-plugin-import";
4+
import pluginPrettier from "eslint-plugin-prettier";
5+
import globals from "globals";
6+
import ts from "typescript-eslint";
7+
8+
export default defineConfig([
9+
js.configs.recommended,
10+
ts.configs.recommended,
11+
pluginImport.flatConfigs.recommended,
12+
pluginImport.flatConfigs.typescript,
13+
globalIgnores([
14+
// Ignore generated directories
15+
"node_modules/",
16+
"public/",
17+
// Ignore vendored code
18+
"static/*.js",
19+
// Don't ignore JS and TS dotfiles in this folder
20+
"!.*.js",
21+
"!.*.ts",
22+
]),
23+
{
24+
plugins: {
25+
prettier: pluginPrettier,
26+
},
27+
settings: {
28+
"import/parsers": { "@typescript-eslint/parser": [".ts", ".js"] },
29+
"import/resolver": { typescript: true, node: true },
30+
},
31+
languageOptions: {
32+
parserOptions: {
33+
project: "./tsconfig.json",
34+
},
35+
globals: {
36+
...globals.browser,
37+
...globals.node,
38+
},
39+
},
40+
rules: {
41+
// Standard ESLint config (for ordinary JS syntax linting)
42+
indent: "off",
43+
quotes: ["error", "double", { allowTemplateLiterals: true }],
44+
camelcase: ["error", { properties: "always" }],
45+
curly: ["error", "multi-line"],
46+
"linebreak-style": ["error", "unix"],
47+
"eol-last": ["error", "always"],
48+
"max-len": ["error", { code: 200, tabWidth: 4, ignorePattern: `d="([\\s\\S]*?)"` }],
49+
"prefer-destructuring": "off",
50+
"no-console": "warn",
51+
"no-debugger": "warn",
52+
"no-param-reassign": ["error", { props: false }],
53+
"no-bitwise": "off",
54+
"no-shadow": "off",
55+
"no-use-before-define": "off",
56+
"no-restricted-imports": ["error", { patterns: [".*"] }],
57+
58+
// TypeScript plugin config (for TS-specific linting)
59+
"@typescript-eslint/indent": "off",
60+
"@typescript-eslint/camelcase": "off",
61+
"@typescript-eslint/no-use-before-define": "off",
62+
"@typescript-eslint/no-unused-vars": [
63+
"error",
64+
{
65+
args: "all",
66+
argsIgnorePattern: "^_",
67+
caughtErrors: "all",
68+
caughtErrorsIgnorePattern: "^_",
69+
destructuredArrayIgnorePattern: "^_",
70+
varsIgnorePattern: "^_",
71+
ignoreRestSiblings: true,
72+
},
73+
],
74+
"@typescript-eslint/consistent-type-imports": "error",
75+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
76+
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as", objectLiteralTypeAssertions: "never" }],
77+
"@typescript-eslint/consistent-indexed-object-style": ["error", "record"],
78+
"@typescript-eslint/consistent-generic-constructors": ["error", "constructor"],
79+
"@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }],
80+
81+
// Import plugin config (for intelligently validating module import statements)
82+
"import/no-unresolved": "error",
83+
"import/prefer-default-export": "off",
84+
"import/no-relative-packages": "error",
85+
"import/no-named-as-default-member": "off",
86+
"import/order": [
87+
"error",
88+
{
89+
alphabetize: { order: "asc", caseInsensitive: true },
90+
warnOnUnassignedImports: true,
91+
"newlines-between": "always-and-inside-groups",
92+
},
93+
],
94+
95+
// Prettier plugin config (for validating and fixing formatting)
96+
"prettier/prettier": [
97+
"error",
98+
{
99+
tabWidth: 4,
100+
tabs: true,
101+
printWidth: 200,
102+
},
103+
],
104+
},
105+
},
106+
]);

0 commit comments

Comments
 (0)