From 9cfd61fabbd67b537bef4ab0cca58731ceaa7117 Mon Sep 17 00:00:00 2001 From: FurryR Date: Thu, 4 Jul 2024 18:48:10 +0800 Subject: [PATCH] bump version & esm & async & implement #1587 --- .gitignore | 1 + development/build-production.js | 16 +- development/builder.js | 228 ++++++++++++-------- development/colors.js | 10 +- development/compatibility-aliases.js | 2 +- development/fs-utils.js | 23 +- development/get-credits-for-gui.js | 63 ++---- development/parse-extension-metadata.js | 2 +- development/parse-extension-translations.js | 8 +- development/render-docs.js | 15 +- development/render-template.js | 12 +- development/server.js | 10 +- development/validate.js | 18 +- eslint.config.js | 15 +- package-lock.json | 171 ++++++++------- package.json | 1 + tsconfig.json | 2 +- 17 files changed, 323 insertions(+), 274 deletions(-) diff --git a/.gitignore b/.gitignore index b6b3ebdff6..b29fce3fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules # Final built website and localizations build build-l10n +cache # Various operating system caches thumbs.db diff --git a/development/build-production.js b/development/build-production.js index acfe878b40..6374a68437 100644 --- a/development/build-production.js +++ b/development/build-production.js @@ -1,14 +1,16 @@ -const pathUtil = require("path"); -const Builder = require("./builder"); +import pathUtil from "node:path"; +import Builder from "./builder.js"; +import urlUtil from "node:url"; -const outputDirectory = pathUtil.join(__dirname, "../build"); -const l10nOutput = pathUtil.join(__dirname, "../build-l10n"); +const dirname = pathUtil.dirname(urlUtil.fileURLToPath(import.meta.url)); +const outputDirectory = pathUtil.join(dirname, "../build"); +const l10nOutput = pathUtil.join(dirname, "../build-l10n"); const builder = new Builder("production"); -const build = builder.build(); +const build = await builder.build(); -build.export(outputDirectory); +await build.export(outputDirectory); console.log(`Built to ${outputDirectory}`); -build.exportL10N(l10nOutput); +await build.exportL10N(l10nOutput); console.log(`Exported L10N to ${l10nOutput}`); diff --git a/development/builder.js b/development/builder.js index 2b2bdf8a84..d4a43f204f 100644 --- a/development/builder.js +++ b/development/builder.js @@ -1,17 +1,23 @@ -const fs = require("fs"); -const AdmZip = require("adm-zip"); -const pathUtil = require("path"); -const ExtendedJSON = require("@turbowarp/json"); -const compatibilityAliases = require("./compatibility-aliases"); -const parseMetadata = require("./parse-extension-metadata"); -const { mkdirp, recursiveReadDirectory } = require("./fs-utils"); +import fs from "node:fs/promises"; +import AdmZip from "adm-zip"; +import pathUtil from "node:path"; +import ExtendedJSON from "@turbowarp/json"; +import compatibilityAliases from "./compatibility-aliases.js"; +import parseMetadata from "./parse-extension-metadata.js"; +import { mkdirp, recursiveReadDirectory } from "./fs-utils.js"; +import * as espree from "espree"; +import esquery from "esquery"; +import { createHash } from "crypto"; +import urlUtil from "node:url"; + +const dirname = pathUtil.dirname(urlUtil.fileURLToPath(import.meta.url)); /** * @typedef {'development'|'production'|'desktop'} Mode */ /** - * @typedef TranslatableString + * @typedef TranslatableStringretract * @property {string} string The English version of the string * @property {string} developer_comment Helper text to help translators */ @@ -110,12 +116,12 @@ class BuildFile { return pathUtil.extname(this.sourcePath); } - getLastModified() { - return fs.statSync(this.sourcePath).mtimeMs; + async getLastModified() { + return (await fs.stat(this.sourcePath)).mtimeMs; } read() { - return fs.readFileSync(this.sourcePath); + return fs.readFile(this.sourcePath); } validate() { @@ -123,11 +129,10 @@ class BuildFile { } /** - * @returns {Record>|null} + * @returns {Record> | null | Promise> | null>} */ getStrings() { // no-op by default, to be overridden - return null; } } @@ -151,14 +156,62 @@ class ExtensionFile extends BuildFile { this.mode = mode; } - read() { - const data = fs.readFileSync(this.sourcePath, "utf-8"); + async read() { + let data = await fs.readFile(this.sourcePath, "utf-8"); if (this.mode !== "development") { const translations = filterTranslationsByPrefix( this.allTranslations, `${this.slug}@` ); + + // This process only runs on production builds. + + const ast = espree.parse(data, { + ecmaVersion: 2022, + sourceType: "script", + }); + /** @type {Set} */ + const dependencies = new Set(); + const selector = esquery.parse("ImportExpression"); + const matches = esquery.match(ast, selector); + for (const match of matches) { + if (match.source.type === "Literal") { + const value = match.source.value; + if (typeof value === "string") { + dependencies.add(value); + } else { + throw new Error( + `${this.sourcePath}: Invalid URL\n at char ${match.source.start}~${match.source.end}` + ); + } + } else { + throw new Error( + `${this.sourcePath}: Dynamic URL is not supported\n at char ${match.source.start}~${match.source.end}` + ); + } + } + await mkdirp("cache"); + if (dependencies.size > 0) + console.group(`Processing dependencies for ${this.sourcePath}`); + for (const url of dependencies) { + const filename = createHash("sha256") + .setEncoding("utf-8") + .update(url) + .digest("hex"); + let dependency; + try { + dependency = `data:text/javascript;base64,${await fs.readFile(`cache/${filename}.js`, "base64")}`; + console.log(`Using cached ${url} (${filename}.js)`); + } catch { + console.log(`Downloading ${url}`); + const data = Buffer.from(await (await fetch(url)).arrayBuffer()); + await fs.writeFile(`cache/${filename}.js`, data); + dependency = `data:text/javascript;base64,${data.toString("base64")}`; + } + data = data.replaceAll(url, dependency); + } + if (dependencies.size > 0) console.groupEnd(); if (translations !== null) { return insertAfterCommentsBeforeCode( data, @@ -172,17 +225,17 @@ class ExtensionFile extends BuildFile { return data; } - getMetadata() { - const data = fs.readFileSync(this.sourcePath, "utf-8"); + async getMetadata() { + const data = await fs.readFile(this.sourcePath, "utf-8"); return parseMetadata(data); } - validate() { + async validate() { if (!this.featured) { return; } - const metadata = this.getMetadata(); + const metadata = await this.getMetadata(); if (!metadata.id) { throw new Error("Missing // ID:"); @@ -213,7 +266,7 @@ class ExtensionFile extends BuildFile { ); } - const spdxParser = require("spdx-expression-parse"); + const spdxParser = (await import("spdx-expression-parse")).default; try { // Don't care about the result -- just see if it parses. spdxParser(metadata.license); @@ -238,12 +291,12 @@ class ExtensionFile extends BuildFile { } } - getStrings() { + async getStrings() { if (!this.featured) { return null; } - const metadata = this.getMetadata(); + const metadata = await this.getMetadata(); const slug = this.slug; const getMetadataDescription = (part) => { @@ -264,8 +317,10 @@ class ExtensionFile extends BuildFile { }, }; - const parseTranslations = require("./parse-extension-translations"); - const jsCode = fs.readFileSync(this.sourcePath, "utf-8"); + const parseTranslations = ( + await import("./parse-extension-translations.js") + ).default; + const jsCode = await fs.readFile(this.sourcePath, "utf-8"); const unprefixedRuntimeStrings = parseTranslations(jsCode); const runtimeStrings = Object.fromEntries( Object.entries(unprefixedRuntimeStrings).map(([key, value]) => [ @@ -290,7 +345,7 @@ class HomepageFile extends BuildFile { samples, mode ) { - super(pathUtil.join(__dirname, "homepage-template.ejs")); + super(pathUtil.join(dirname, "homepage-template.ejs")); /** @type {Record} */ this.extensionFiles = extensionFiles; @@ -343,23 +398,28 @@ class HomepageFile extends BuildFile { return `https://turbowarp.org/editor?project_url=${this.host}${path}`; } - read() { - const renderTemplate = require("./render-template"); + async read() { + const renderTemplate = (await import("./render-template.js")).default; const mostRecentExtensions = Object.entries(this.extensionFiles) .sort((a, b) => b[1].getLastModified() - a[1].getLastModified()) .slice(0, 5) .map((i) => i[0]); - const extensionMetadata = Object.fromEntries( - this.featuredSlugs.map((slug) => [ - slug, - { - ...this.extensionFiles[slug].getMetadata(), - hasDocumentation: this.withDocs.has(slug), - samples: this.samples.get(slug) || [], - }, - ]) + await (async () => { + const result = []; + for (const slug of this.featuredSlugs) { + result.push([ + slug, + { + ...(await this.extensionFiles[slug].getMetadata()), + hasDocumentation: this.withDocs.has(slug), + samples: this.samples.get(slug) || [], + }, + ]); + } + return result; + })() ); return renderTemplate(this.sourcePath, { @@ -409,12 +469,12 @@ class JSONMetadataFile extends BuildFile { return ".json"; } - read() { + async read() { const extensions = []; for (const extensionSlug of this.featuredSlugs) { const extension = {}; const file = this.extensionFiles[extensionSlug]; - const metadata = file.getMetadata(); + const metadata = await file.getMetadata(); const image = this.extensionImages[extensionSlug]; extension.slug = extensionSlug; @@ -469,9 +529,9 @@ class JSONMetadataFile extends BuildFile { } class ImageFile extends BuildFile { - validate() { - const sizeOfImage = require("image-size"); - const contents = this.read(); + async validate() { + const sizeOfImage = (await import("image-size")).default; + const contents = await this.read(); const { width, height } = sizeOfImage(contents); const aspectRatio = width / height; if (aspectRatio !== 2) { @@ -485,15 +545,15 @@ class ImageFile extends BuildFile { } class SVGFile extends ImageFile { - validate() { - const contents = this.read(); + async validate() { + const contents = await this.read(); if (contents.includes(" elements -- please convert the text to a path. This ensures it will display correctly on all devices." ); } - super.validate(); + await super.validate(); } } @@ -540,9 +600,9 @@ class DocsFile extends BuildFile { this.extensionSlug = extensionSlug; } - read() { - const renderDocs = require("./render-docs"); - const markdown = super.read().toString("utf-8"); + async read() { + const renderDocs = (await import("./render-docs.js")).default; + const markdown = (await super.read()).toString("utf-8"); return renderDocs(markdown, this.extensionSlug); } @@ -604,28 +664,28 @@ class Build { ); } - export(root) { + async export(root) { mkdirp(root); for (const [relativePath, file] of Object.entries(this.files)) { const directoryName = pathUtil.dirname(relativePath); - fs.mkdirSync(pathUtil.join(root, directoryName), { + await fs.mkdir(pathUtil.join(root, directoryName), { recursive: true, }); - fs.writeFileSync(pathUtil.join(root, relativePath), file.read()); + await fs.writeFile(pathUtil.join(root, relativePath), await file.read()); } } /** - * @returns {Record>} + * @returns {Promise>>} */ - generateL10N() { + async generateL10N() { const allStrings = {}; for (const [filePath, file] of Object.entries(this.files)) { let fileStrings; try { - fileStrings = file.getStrings(); + fileStrings = await file.getStrings(); } catch (error) { console.error(error); throw new Error( @@ -658,13 +718,13 @@ class Build { /** * @param {string} root */ - exportL10N(root) { - mkdirp(root); + async exportL10N(root) { + await mkdirp(root); - const groups = this.generateL10N(); + const groups = await this.generateL10N(); for (const [name, strings] of Object.entries(groups)) { const filename = pathUtil.join(root, `exported-${name}.json`); - fs.writeFileSync(filename, JSON.stringify(strings, null, 2)); + await fs.writeFile(filename, JSON.stringify(strings, null, 2)); } } } @@ -685,19 +745,19 @@ class Builder { this.mode = mode; } - this.extensionsRoot = pathUtil.join(__dirname, "../extensions"); - this.websiteRoot = pathUtil.join(__dirname, "../website"); - this.imagesRoot = pathUtil.join(__dirname, "../images"); - this.docsRoot = pathUtil.join(__dirname, "../docs"); - this.samplesRoot = pathUtil.join(__dirname, "../samples"); - this.translationsRoot = pathUtil.join(__dirname, "../translations"); + this.extensionsRoot = pathUtil.join(dirname, "../extensions"); + this.websiteRoot = pathUtil.join(dirname, "../website"); + this.imagesRoot = pathUtil.join(dirname, "../images"); + this.docsRoot = pathUtil.join(dirname, "../docs"); + this.samplesRoot = pathUtil.join(dirname, "../samples"); + this.translationsRoot = pathUtil.join(dirname, "../translations"); } - build() { + async build() { const build = new Build(this.mode); const featuredExtensionSlugs = ExtendedJSON.parse( - fs.readFileSync( + await fs.readFile( pathUtil.join(this.extensionsRoot, "extensions.json"), "utf-8" ) @@ -708,20 +768,20 @@ class Builder { * @type {Record>>} */ const translations = {}; - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.translationsRoot )) { if (!filename.endsWith(".json")) { continue; } const group = filename.split(".")[0]; - const data = JSON.parse(fs.readFileSync(absolutePath, "utf-8")); + const data = JSON.parse(await fs.readFile(absolutePath, "utf-8")); translations[group] = data; } /** @type {Record} */ const extensionFiles = {}; - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.extensionsRoot )) { if (!filename.endsWith(".js")) { @@ -742,7 +802,7 @@ class Builder { /** @type {Record} */ const extensionImages = {}; - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.imagesRoot )) { const extension = pathUtil.extname(filename); @@ -762,7 +822,7 @@ class Builder { /** @type {Map} */ const samples = new Map(); - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.samplesRoot )) { if (!filename.endsWith(".sb3")) { @@ -781,14 +841,14 @@ class Builder { build.files[`/samples/${filename}`] = file; } - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.websiteRoot )) { build.files[`/${filename}`] = new BuildFile(absolutePath); } if (this.mode !== "desktop") { - for (const [filename, absolutePath] of recursiveReadDirectory( + for (const [filename, absolutePath] of await recursiveReadDirectory( this.docsRoot )) { if (!filename.endsWith(".md")) { @@ -801,7 +861,7 @@ class Builder { } const scratchblocksPath = pathUtil.join( - __dirname, + dirname, "../node_modules/@turbowarp/scratchblocks/build/scratchblocks.min.js" ); build.files["/docs-internal/scratchblocks.js"] = new BuildFile( @@ -836,12 +896,12 @@ class Builder { return build; } - tryBuild(...args) { + async tryBuild(...args) { const start = new Date(); process.stdout.write(`[${start.toLocaleTimeString()}] Building... `); try { - const build = this.build(...args); + const build = await this.build(...args); const time = Date.now() - start.getTime(); console.log(`done in ${time}ms`); return build; @@ -853,10 +913,10 @@ class Builder { return null; } - startWatcher(callback) { + async startWatcher(callback) { // Load chokidar lazily. - const chokidar = require("chokidar"); - callback(this.tryBuild()); + const chokidar = (await import("chokidar")).default; + callback(await this.tryBuild()); chokidar .watch( [ @@ -871,17 +931,17 @@ class Builder { ignoreInitial: true, } ) - .on("all", () => { - callback(this.tryBuild()); + .on("all", async () => { + callback(await this.tryBuild()); }); } - validate() { + async validate() { const errors = []; - const build = this.build(); + const build = await this.build(); for (const [fileName, file] of Object.entries(build.files)) { try { - file.validate(); + await file.validate(); } catch (e) { errors.push({ fileName, @@ -893,4 +953,4 @@ class Builder { } } -module.exports = Builder; +export default Builder; diff --git a/development/colors.js b/development/colors.js index 7e6fa1eaa8..eeceb2ff54 100644 --- a/development/colors.js +++ b/development/colors.js @@ -1,9 +1,7 @@ const enableColor = !process.env.NO_COLOR; const color = (i) => (enableColor ? i : ""); -module.exports = { - RESET: color("\x1b[0m"), - BOLD: color("\x1b[1m"), - RED: color("\x1b[31m"), - GREEN: color("\x1b[32m"), -}; +export const RESET = color("\x1b[0m"); +export const BOLD = color("\x1b[1m"); +export const RED = color("\x1b[31m"); +export const GREEN = color("\x1b[32m"); diff --git a/development/compatibility-aliases.js b/development/compatibility-aliases.js index e1722787fa..37c90cac32 100644 --- a/development/compatibility-aliases.js +++ b/development/compatibility-aliases.js @@ -10,4 +10,4 @@ const extensions = { "/LukeManiaStudios/TempVariables2.js": "/Lily/TempVariables2.js", }; -module.exports = extensions; +export default extensions; diff --git a/development/fs-utils.js b/development/fs-utils.js index 020a1e9d98..9f289cb817 100644 --- a/development/fs-utils.js +++ b/development/fs-utils.js @@ -1,26 +1,26 @@ -const fs = require("fs"); -const pathUtil = require("path"); +import * as fs from "node:fs/promises"; +import pathUtil from "node:path"; /** * Recursively read a directory. * @param {string} directory - * @returns {Array<[string, string]>} List of tuples [name, absolutePath]. + * @returns {Promise>} List of tuples [name, absolutePath]. * The return result includes files in subdirectories, but not the subdirectories themselves. */ -const recursiveReadDirectory = (directory) => { +const recursiveReadDirectory = async (directory) => { const result = []; - for (const name of fs.readdirSync(directory)) { + for (const name of await fs.readdir(directory)) { if (name.startsWith(".")) { // Ignore .eslintrc.js, .DS_Store, etc. continue; } const absolutePath = pathUtil.join(directory, name); - const stat = fs.statSync(absolutePath); + const stat = await fs.stat(absolutePath); if (stat.isDirectory()) { for (const [ relativeToChildName, childAbsolutePath, - ] of recursiveReadDirectory(absolutePath)) { + ] of await recursiveReadDirectory(absolutePath)) { // This always needs to use / on all systems result.push([`${name}/${relativeToChildName}`, childAbsolutePath]); } @@ -35,9 +35,9 @@ const recursiveReadDirectory = (directory) => { * Synchronous create a directory and any parents. Does nothing if the folder already exists. * @param {string} directory */ -const mkdirp = (directory) => { +const mkdirp = async (directory) => { try { - fs.mkdirSync(directory, { + await fs.mkdir(directory, { recursive: true, }); } catch (e) { @@ -47,7 +47,4 @@ const mkdirp = (directory) => { } }; -module.exports = { - recursiveReadDirectory, - mkdirp, -}; +export { recursiveReadDirectory, mkdirp }; diff --git a/development/get-credits-for-gui.js b/development/get-credits-for-gui.js index 22465e525c..b1d734a740 100644 --- a/development/get-credits-for-gui.js +++ b/development/get-credits-for-gui.js @@ -1,8 +1,10 @@ -const fs = require("fs"); -const path = require("path"); -const https = require("https"); -const fsUtils = require("./fs-utils"); -const parseMetadata = require("./parse-extension-metadata"); +import { readFile } from "node:fs/promises"; +import { recursiveReadDirectory } from "./fs-utils.js"; +import parseMetadata from "./parse-extension-metadata.js"; +import pathUtil from "node:path"; +import urlUtil from "node:url"; + +const dirname = pathUtil.dirname(urlUtil.fileURLToPath(import.meta.url)); class AggregatePersonInfo { /** @param {Person} person */ @@ -23,38 +25,19 @@ class AggregatePersonInfo { * @param {string} username * @returns {Promise} */ -const getUserID = (username) => - new Promise((resolve, reject) => { - process.stdout.write(`Getting user ID for ${username}... `); - const request = https.get(`https://api.scratch.mit.edu/users/${username}`); - - request.on("response", (response) => { - const data = []; - response.on("data", (newData) => { - data.push(newData); - }); - - response.on("end", () => { - const allData = Buffer.concat(data); - const json = JSON.parse(allData.toString("utf-8")); - const userID = String(json.id); - process.stdout.write(`${userID}\n`); - resolve(userID); - }); - - response.on("error", (error) => { - process.stdout.write("error\n"); - reject(error); - }); - }); - - request.on("error", (error) => { - process.stdout.write("error\n"); - reject(error); - }); - - request.end(); - }); +const getUserID = async (username) => { + process.stdout.write(`Getting user ID for ${username}... `); + const request = await fetch(`https://api.scratch.mit.edu/users/${username}`); + try { + const json = await request.json(); + const userID = String(json.id); + process.stdout.write(`${userID}\n`); + return userID; + } catch (e) { + process.stdout.write("error\n"); + throw e; + } +}; const run = async () => { /** @@ -62,15 +45,15 @@ const run = async () => { */ const aggregate = new Map(); - const extensionRoot = path.resolve(__dirname, "../extensions/"); - for (const [name, absolutePath] of fsUtils.recursiveReadDirectory( + const extensionRoot = pathUtil.resolve(dirname, "../extensions/"); + for (const [name, absolutePath] of await recursiveReadDirectory( extensionRoot )) { if (!name.endsWith(".js")) { continue; } - const code = fs.readFileSync(absolutePath, "utf-8"); + const code = await readFile(absolutePath, "utf-8"); const metadata = parseMetadata(code); for (const person of [...metadata.by, ...metadata.original]) { diff --git a/development/parse-extension-metadata.js b/development/parse-extension-metadata.js index fd97a62cad..3d5356e14b 100644 --- a/development/parse-extension-metadata.js +++ b/development/parse-extension-metadata.js @@ -111,4 +111,4 @@ const parseMetadata = (extensionCode) => { return metadata; }; -module.exports = parseMetadata; +export default parseMetadata; diff --git a/development/parse-extension-translations.js b/development/parse-extension-translations.js index 55e2570ff7..625451fadc 100644 --- a/development/parse-extension-translations.js +++ b/development/parse-extension-translations.js @@ -1,6 +1,6 @@ -const espree = require("espree"); -const esquery = require("esquery"); -const parseMetadata = require("./parse-extension-metadata"); +import * as espree from 'espree' +import esquery from 'esquery' +import parseMetadata from "./parse-extension-metadata.js"; /** * @fileoverview Parses extension code to find calls to Scratch.translate() and statically @@ -120,4 +120,4 @@ const parseTranslations = (js) => { return result; }; -module.exports = parseTranslations; +export default parseTranslations; diff --git a/development/render-docs.js b/development/render-docs.js index 3bdcfab88e..531d0bd179 100644 --- a/development/render-docs.js +++ b/development/render-docs.js @@ -1,6 +1,7 @@ -const path = require("path"); -const MarkdownIt = require("markdown-it"); -const renderTemplate = require("./render-template"); +import pathUtil from "node:path"; +import MarkdownIt from "markdown-it"; +import renderTemplate from "./render-template.js"; +import urlUtil from "node:url"; const md = new MarkdownIt({ html: true, @@ -8,6 +9,8 @@ const md = new MarkdownIt({ breaks: true, }); +const dirname = pathUtil.dirname(urlUtil.fileURLToPath(import.meta.url)); + md.renderer.rules.fence = function (tokens, idx, options, env, self) { const token = tokens[idx]; @@ -28,7 +31,7 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) { /** * @param {string} markdownSource Markdown code * @param {string} slug Path slug like 'TestMuffin/fetch' - * @returns {string} HTML source code + * @returns {Promise} HTML source code */ const renderDocs = (markdownSource, slug) => { const env = {}; @@ -63,7 +66,7 @@ const renderDocs = (markdownSource, slug) => { const bodyHTML = md.renderer.render(tokens, md.options, env); - return renderTemplate(path.join(__dirname, "docs-template.ejs"), { + return renderTemplate(pathUtil.join(dirname, "docs-template.ejs"), { slug, headerHTML, headerText, @@ -72,4 +75,4 @@ const renderDocs = (markdownSource, slug) => { }); }; -module.exports = renderDocs; +export default renderDocs; diff --git a/development/render-template.js b/development/render-template.js index 41f036b4d1..924e6ed4b7 100644 --- a/development/render-template.js +++ b/development/render-template.js @@ -1,12 +1,12 @@ -const fs = require("fs"); -const ejs = require("ejs"); +import { readFile } from "node:fs/promises"; +import { render } from "ejs"; // TODO: Investigate the value of removing dependency on `ejs` and possibly writing our own DSL. -const renderTemplate = (path, data) => { - const inputEJS = fs.readFileSync(path, "utf-8"); - const outputHTML = ejs.render(inputEJS, data); +const renderTemplate = async (path, data) => { + const inputEJS = await readFile(path, "utf-8"); + const outputHTML = render(inputEJS, data); return outputHTML; }; -module.exports = renderTemplate; +export default renderTemplate; diff --git a/development/server.js b/development/server.js index 0809389d35..4c10f66e91 100644 --- a/development/server.js +++ b/development/server.js @@ -1,9 +1,9 @@ -const express = require("express"); -const Builder = require("./builder"); +import express from "express"; +import Builder from "./builder.mjs"; let mostRecentBuild = null; const builder = new Builder("development"); -builder.startWatcher((newBuild) => { +await builder.startWatcher((newBuild) => { mostRecentBuild = newBuild; }); @@ -36,7 +36,7 @@ app.use((req, res, next) => { next(); }); -app.get("/*", (req, res, next) => { +app.get("/*", async (req, res, next) => { if (!mostRecentBuild) { res.contentType("text/plain"); res.status(500); @@ -50,7 +50,7 @@ app.get("/*", (req, res, next) => { } res.contentType(fileInBuild.getType()); - res.send(fileInBuild.read()); + res.send(await fileInBuild.read()); }); app.use((req, res) => { diff --git a/development/validate.js b/development/validate.js index 64bf9496ab..5e3037a84d 100644 --- a/development/validate.js +++ b/development/validate.js @@ -1,23 +1,21 @@ -const Builder = require("./builder"); -const Colors = require("./colors"); +import Builder from "./builder.mjs"; +import { GREEN, BOLD, RESET, RED } from "./colors.mjs"; -const builder = new Builder("production"); -const errors = builder.validate(); +const builder = new Builder("development"); +const errors = await builder.validate(); if (errors.length === 0) { - console.log( - `${Colors.GREEN}${Colors.BOLD}Validation checks passed.${Colors.RESET}` - ); + console.log(`${GREEN}${BOLD}Validation checks passed.${RESET}`); process.exit(0); } else { console.error( - `${Colors.RED}${Colors.BOLD}${errors.length} ${ + `${RED}${BOLD}${errors.length} ${ errors.length === 1 ? "file" : "files" - } failed validation.${Colors.RESET}` + } failed validation.${RESET}` ); console.error(""); for (const { fileName, error } of errors) { - console.error(`${Colors.BOLD}${fileName}${Colors.RESET}: ${error}`); + console.error(`${BOLD}${fileName}${RESET}: ${error}`); } console.error(``); process.exit(1); diff --git a/eslint.config.js b/eslint.config.js index 4e26f68ae4..eff7a57210 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,15 +1,14 @@ -const js = require('@eslint/js'); -const globals = require('globals'); +import js from '@eslint/js'; +import globals from 'globals'; -module.exports = [ +export default [ // Base on eslint recommended js.configs.recommended, // Common for all files { languageOptions: { - ecmaVersion: 2022, - sourceType: 'commonjs' + ecmaVersion: 2022 }, rules: { // Unused variables commonly indicate logic errors @@ -78,7 +77,8 @@ module.exports = [ globals: { ...globals.commonjs, ...globals.node - } + }, + sourceType: 'module' } }, @@ -95,7 +95,8 @@ module.exports = [ ScratchBlocks: 'readonly', ScratchExtensions: 'readonly', scaffolding: 'readonly' - } + }, + sourceType: 'script' }, rules: { // Require each extension to use strict mode diff --git a/package-lock.json b/package-lock.json index 2341936b0f..eb4175fa41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,15 +27,6 @@ "spdx-expression-parse": "^4.0.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -51,10 +42,22 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -97,25 +100,13 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -218,7 +209,6 @@ "node_modules/@turbowarp/types": { "version": "0.0.12", "resolved": "git+ssh://git@github.com/TurboWarp/types-tw.git#da533389791e5eace0fbb8d3a66b5eb79b186ee4", - "integrity": "sha512-CNfaI0MxTEr9fbXk7DHmcdmYscmn8e1rGuqiX+25fFV2DH9cD0CJSzzVIGm2Lwf24RdcAAF1lDAmogs/gHIy6A==", "license": "Apache-2.0" }, "node_modules/accepts": { @@ -234,9 +224,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -334,11 +324,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { @@ -387,11 +380,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -538,9 +531,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -735,18 +728,6 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", @@ -804,6 +785,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -927,9 +920,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -975,9 +968,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1181,9 +1174,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -1332,9 +1325,9 @@ "dev": true }, "node_modules/jake": { - "version": "10.8.7", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", - "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -1541,9 +1534,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1560,17 +1556,17 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -1910,16 +1906,16 @@ } }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1985,9 +1981,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/statuses": { @@ -2135,6 +2131,15 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 4da8a6687f..765a2b20c9 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "@turbowarp/extensions", "version": "0.0.1", "description": "Unsandboxed extensions for TurboWarp", + "type": "module", "scripts": { "start": "node development/server.js", "dev": "node development/server.js", diff --git a/tsconfig.json b/tsconfig.json index 521d69ccf6..ed9c88d575 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "noEmit": true, "allowJs": true, "checkJs": true,