From 4c2c8cad132108d5121c3cf81f87d088988a2307 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 03:16:10 +0800 Subject: [PATCH 01/51] Moved to using gulp for task automation --- gulpfile.esm.js/.eslintrc.js | 70 + .../docs => gulpfile.esm.js/build}/README.md | 0 gulpfile.esm.js/build/bundles.js | 59 + .../build}/constants.js | 0 gulpfile.esm.js/build/copyfile.js | 12 + gulpfile.esm.js/build/database.json | 30 + gulpfile.esm.js/build/docs/drawdown.js | 190 ++ gulpfile.esm.js/build/docs/index.js | 166 ++ gulpfile.esm.js/build/index.js | 20 + gulpfile.esm.js/build/tabs.js | 62 + gulpfile.esm.js/build/utilities.js | 141 ++ gulpfile.esm.js/index.js | 7 + package.json | 29 +- scripts/docs/index.js | 30 - scripts/docs/paths.js | 7 - scripts/rollup/rollup.config.js | 35 - scripts/rollup/utilities.js | 236 --- scripts/templates/app.js | 29 - scripts/templates/module.js | 43 - scripts/templates/paths.js | 19 - scripts/templates/print.js | 39 - scripts/templates/tab.js | 68 - scripts/templates/templates/__bundle__.ts | 34 - scripts/templates/templates/__tab__.tsx | 71 - scripts/templates/utilities.js | 15 - .eslintrc.js => src/.eslintrc.js | 3 +- src/bundles/curve/index.ts | 3 + tsconfig.json => src/tsconfig.json | 4 +- yarn.lock | 1603 ++++++++++++++++- 29 files changed, 2343 insertions(+), 682 deletions(-) create mode 100644 gulpfile.esm.js/.eslintrc.js rename {scripts/docs => gulpfile.esm.js/build}/README.md (100%) create mode 100644 gulpfile.esm.js/build/bundles.js rename {scripts/rollup => gulpfile.esm.js/build}/constants.js (100%) create mode 100644 gulpfile.esm.js/build/copyfile.js create mode 100644 gulpfile.esm.js/build/database.json create mode 100644 gulpfile.esm.js/build/docs/drawdown.js create mode 100644 gulpfile.esm.js/build/docs/index.js create mode 100644 gulpfile.esm.js/build/index.js create mode 100644 gulpfile.esm.js/build/tabs.js create mode 100644 gulpfile.esm.js/build/utilities.js create mode 100644 gulpfile.esm.js/index.js delete mode 100644 scripts/docs/index.js delete mode 100644 scripts/docs/paths.js delete mode 100644 scripts/rollup/rollup.config.js delete mode 100644 scripts/rollup/utilities.js delete mode 100644 scripts/templates/app.js delete mode 100644 scripts/templates/module.js delete mode 100644 scripts/templates/paths.js delete mode 100644 scripts/templates/print.js delete mode 100644 scripts/templates/tab.js delete mode 100644 scripts/templates/templates/__bundle__.ts delete mode 100644 scripts/templates/templates/__tab__.tsx delete mode 100644 scripts/templates/utilities.js rename .eslintrc.js => src/.eslintrc.js (97%) rename tsconfig.json => src/tsconfig.json (96%) diff --git a/gulpfile.esm.js/.eslintrc.js b/gulpfile.esm.js/.eslintrc.js new file mode 100644 index 000000000..9d1e71885 --- /dev/null +++ b/gulpfile.esm.js/.eslintrc.js @@ -0,0 +1,70 @@ +module.exports = { + extends: ['../.eslintrc.base.js', 'plugin:prettier/recommended'], + + root: true, + parserOptions: { + sourceType: 'module', + }, + + rules: { + 'func-style': 0, + indent: [ + 1, + 2, // Was "tabs" + { + SwitchCase: 1, // Same + // VariableDeclarator: 1, + // outerIIFEBody: 1, + // MemberExpression: 1, + // FunctionDeclaration: { + // parameters: 1, + // body: 1 + // }, + // FunctionExpression: { + // parameters: 1, + // body: 1 + // }, + // StaticBlock: { + // body: 1 + // }, + // CallExpression: { + // arguments: 1, + // }, + // ArrayExpression: 1, + // ObjectExpression: 1, + // ImportDeclaration: 1, + // flatTernaryExpressions: false, + // offsetTernaryExpressions: false, + // ignoreComments: false + }, + ], + quotes: [ + 1, + 'single', // Was "double" + { + avoidEscape: true, // Same + // allowTemplateLiterals: false + }, + ], + + 'prettier/prettier': 1, // Was 2 + }, + + overrides: [ + { + files: ['**/**.js'], + extends: ['airbnb', 'plugin:prettier/recommended'], + parserOptions: { + ecmaVersion: '2020', + }, + rules: { + 'import/no-extraneous-dependencies': 0, + 'no-console': 0, + 'no-param-reassign': 0, + 'no-restricted-syntax': 0, + 'prefer-const': 0, + 'prettier/prettier': 1, // Was 2 + }, + }, + ], +}; diff --git a/scripts/docs/README.md b/gulpfile.esm.js/build/README.md similarity index 100% rename from scripts/docs/README.md rename to gulpfile.esm.js/build/README.md diff --git a/gulpfile.esm.js/build/bundles.js b/gulpfile.esm.js/build/bundles.js new file mode 100644 index 000000000..8b3a69171 --- /dev/null +++ b/gulpfile.esm.js/build/bundles.js @@ -0,0 +1,59 @@ +import chalk from 'chalk'; +import gulp from 'gulp'; +import { rollup } from 'gulp-rollup-2'; +import modules from '../../modules.json'; +import copy from './copyfile'; +import { bundleNameToConfig, getDb, isFolderModified } from './utilities'; + +export const buildBundles = (db) => { + const isBundleModifed = (bundle) => { + if (process.argv[3] === '--force') return true; + const timestamp = db.get(`bundles.${bundle}`).value() || 0; + return isFolderModified(`src/bundles/${bundle}`, timestamp); + }; + + const moduleNames = Object.keys(modules).filter(isBundleModifed); + + if (moduleNames.length === 0) { + console.log('All bundles up to date'); + return null; + } + + console.log(chalk.greenBright('Building bundles for the following modules:')); + console.log( + moduleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') + ); + + const buildTime = new Date().getTime(); + + const promises = moduleNames.map( + (bundle) => + new Promise((resolve, reject) => + // eslint-disable-next-line no-promise-executor-return + gulp + .src(`src/bundles/${bundle}/index.ts`) + .pipe(rollup(bundleNameToConfig(bundle))) + .on('error', reject) + .pipe(gulp.dest('build/bundles/')) + .on('end', () => { + db.set(`bundles.${bundle}`, buildTime).write(); + resolve(); + }) + ) + ); + + return Promise.all(promises); +}; + +export default gulp.series( + Object.assign( + async () => { + const db = await getDb(); + await buildBundles(db); + }, + { + displayName: 'buildBundles', + } + ), + copy +); diff --git a/scripts/rollup/constants.js b/gulpfile.esm.js/build/constants.js similarity index 100% rename from scripts/rollup/constants.js rename to gulpfile.esm.js/build/constants.js diff --git a/gulpfile.esm.js/build/copyfile.js b/gulpfile.esm.js/build/copyfile.js new file mode 100644 index 000000000..dea637b31 --- /dev/null +++ b/gulpfile.esm.js/build/copyfile.js @@ -0,0 +1,12 @@ +import gulp, { series } from 'gulp'; +import { getDbPath } from './utilities'; + +export const copyModules = () => + gulp.src('modules.json').pipe(gulp.dest('build/')); + +export const copyDatabase = () => + gulp.src(getDbPath()).pipe(gulp.dest('build/')); + +export default Object.assign(series(copyModules, copyDatabase), { + displayName: 'copy', +}); diff --git a/gulpfile.esm.js/build/database.json b/gulpfile.esm.js/build/database.json new file mode 100644 index 000000000..c1a8de200 --- /dev/null +++ b/gulpfile.esm.js/build/database.json @@ -0,0 +1,30 @@ +{ + "bundles": { + "copy_gc": 1659718096260, + "mark_sweep": 1659718096260, + "binary_tree": 1659718096260, + "repeat": 1659718096260, + "sound_matrix": 1659718096260, + "scrabble": 1659718096260, + "sound": 1659718096260, + "stereo_sound": 1659718096260, + "pix_n_flix": 1659718096260, + "game": 1659718096260, + "curve": 1659718096260, + "rune": 1659718096260, + "csg": 1659718096260 + }, + "tabs": { + "Repeat": 1659715354944, + "Game": 1659715354944, + "CopyGc": 1659715354944, + "Pixnflix": 1659715354944, + "StereoSound": 1659715354944, + "Sound": 1659715354944, + "SoundMatrix": 1659715354944, + "MarkSweep": 1659715354944, + "Csg": 1659715354944, + "Curve": 1659715354944, + "Rune": 1659715354944 + } +} \ No newline at end of file diff --git a/gulpfile.esm.js/build/docs/drawdown.js b/gulpfile.esm.js/build/docs/drawdown.js new file mode 100644 index 000000000..39e0fe1a3 --- /dev/null +++ b/gulpfile.esm.js/build/docs/drawdown.js @@ -0,0 +1,190 @@ +/* eslint-disable*/ +/** + * Module to convert from markdown into HTML + * drawdown.js + * (c) Adam Leggett + */ + +export default (src) => { + var rx_lt = //g; + var rx_space = /\t|\r|\uf8ff/g; + var rx_escape = /\\([\\\|`*_{}\[\]()#+\-~])/g; + var rx_hr = /^([*\-=_] *){3,}$/gm; + var rx_blockquote = /\n *> *([^]*?)(?=(\n|$){2})/g; + var rx_list = /\n( *)(?:[*\-+]|((\d+)|([a-z])|[A-Z])[.)]) +([^]*?)(?=(\n|$){2})/g; + var rx_listjoin = /<\/(ol|ul)>\n\n<\1>/g; + var rx_highlight = /(^|[^A-Za-z\d\\])(([*_])|(~)|(\^)|(--)|(\+\+)|`)(\2?)([^<]*?)\2\8(?!\2)(?=\W|_|$)/g; + var rx_code = /\n((```|~~~).*\n?([^]*?)\n?\2|(( {4}.*?\n)+))/g; + var rx_link = /((!?)\[(.*?)\]\((.*?)( ".*")?\)|\\([\\`*_{}\[\]()#+\-.!~]))/g; + var rx_table = /\n(( *\|.*?\| *\n)+)/g; + var rx_thead = /^.*\n( *\|( *\:?-+\:?-+\:? *\|)* *\n|)/; + var rx_row = /.*\n/g; + var rx_cell = /\||(.*?[^\\])\|/g; + var rx_heading = /(?=^|>|\n)([>\s]*?)(#{1,6}) (.*?)( #*)? *(?=\n|$)/g; + var rx_para = /(?=^|>|\n)\s*\n+([^<]+?)\n+\s*(?=\n|<|$)/g; + var rx_stash = /-\d+\uf8ff/g; + + function replace(rex, fn) { + src = src.replace(rex, fn); + } + + function element(tag, content) { + return '<' + tag + '>' + content + ''; + } + + function blockquote(src) { + return src.replace(rx_blockquote, function (all, content) { + return element( + 'blockquote', + blockquote(highlight(content.replace(/^ *> */gm, ''))) + ); + }); + } + + function list(src) { + return src.replace(rx_list, function (all, ind, ol, num, low, content) { + var entry = element( + 'li', + highlight( + content + .split( + RegExp('\n ?' + ind + '(?:(?:\\d+|[a-zA-Z])[.)]|[*\\-+]) +', 'g') + ) + .map(list) + .join('
  • ') + ) + ); + + return ( + '\n' + + (ol + ? '
      ' + : parseInt(ol, 36) - + 9 + + '" style="list-style-type:' + + (low ? 'low' : 'upp') + + 'er-alpha">') + + entry + + '
    ' + : element('ul', entry)) + ); + }); + } + + function highlight(src) { + return src.replace( + rx_highlight, + function (all, _, p1, emp, sub, sup, small, big, p2, content) { + return ( + _ + + element( + emp + ? p2 + ? 'strong' + : 'em' + : sub + ? p2 + ? 's' + : 'sub' + : sup + ? 'sup' + : small + ? 'small' + : big + ? 'big' + : 'code', + highlight(content) + ) + ); + } + ); + } + + function unesc(str) { + return str.replace(rx_escape, '$1'); + } + + var stash = []; + var si = 0; + + src = '\n' + src + '\n'; + + replace(rx_lt, '<'); + replace(rx_gt, '>'); + replace(rx_space, ' '); + + // blockquote + src = blockquote(src); + + // horizontal rule + replace(rx_hr, '
    '); + + // list + src = list(src); + replace(rx_listjoin, ''); + + // code + replace(rx_code, function (all, p1, p2, p3, p4) { + stash[--si] = element( + 'pre', + element('code', p3 || p4.replace(/^ {4}/gm, '')) + ); + return si + '\uf8ff'; + }); + + // link or image + replace(rx_link, function (all, p1, p2, p3, p4, p5, p6) { + stash[--si] = p4 + ? p2 + ? '' + p3 + '' + : '' + unesc(highlight(p3)) + '' + : p6; + return si + '\uf8ff'; + }); + + // table + replace(rx_table, function (all, table) { + var sep = table.match(rx_thead)[1]; + return ( + '\n' + + element( + 'table', + table.replace(rx_row, function (row, ri) { + return row == sep + ? '' + : element( + 'tr', + row.replace(rx_cell, function (all, cell, ci) { + return ci + ? element( + sep && !ri ? 'th' : 'td', + unesc(highlight(cell || '')) + ) + : ''; + }) + ); + }) + ) + ); + }); + + // heading + replace(rx_heading, function (all, _, p1, p2) { + return _ + element('h' + p1.length, unesc(highlight(p2))); + }); + + // paragraph + replace(rx_para, function (all, content) { + return element('p', unesc(highlight(content))); + }); + + // stash + replace(rx_stash, function (all) { + return stash[parseInt(all)]; + }); + + return src.trim(); +}; \ No newline at end of file diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js new file mode 100644 index 000000000..9fb7c2dfd --- /dev/null +++ b/gulpfile.esm.js/build/docs/index.js @@ -0,0 +1,166 @@ +import fs from 'fs'; +import gulp from 'gulp'; +import typedoc from 'gulp-typedoc'; +import chalk from 'chalk'; +import drawdown from './drawdown'; +import { isFolderModified, getDb, expandPath } from '../utilities'; +import modules from '../../../modules.json'; + +/** + * Build the json documentation for the specified modules + */ +export async function buildJsons() { + const errHandler = (err) => { + if (err) console.error(err); + }; + + if (!fs.existsSync('build/jsons')) fs.mkdirSync(`build/jsons`, {}); + + const parsers = { + Variable: (element) => { + let desc; + if (element.comment && element.comment.shortText) { + desc = drawdown(element.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${module}: No description found for ${ + element.name + }` + ); + } + + const typeStr = + element.type.name && element.type.name ? `:${element.type.name}` : ''; + + return `

    ${element.name}${typeStr}

    ${desc}
    `; + }, + Function: (element) => { + if (!element.signatures || element.signatures[0] === undefined) + throw new Error( + `Error: ${module}: Unable to find a signature for function ${element.name}!` + ); + + // In source all functions should only have one signature + const signature = element.signatures[0]; + + // Form the parameter string for the function + let paramStr; + if (!signature.parameters) paramStr = `()`; + else + paramStr = `(${signature.parameters + .map((param) => param.name) + .join(', ')})`; + + // Form the result representation for the function + let resultStr; + if (!signature.type) resultStr = `void`; + else resultStr = signature.type.name; + + let desc; + if (signature.comment && signature.comment.shortText) { + desc = drawdown(signature.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${module}: No description found for ${ + element.name + }` + ); + } + + return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; + }, + }; + + // Read from the TypeDoc output and retrieve the JSON relevant to the each module + fs.readFile('build/docs.json', 'utf-8', (err, data) => { + if (err) throw err; + + const parsedJSON = JSON.parse(data).children; + + if (!parsedJSON) { + throw new Error('Failed to parse docs.json'); + } + + const bundles = Object.keys(modules); + + for (const bundle of bundles) { + const moduleDocs = parsedJSON.find((x) => x.name === bundle); + + let output; + if (!moduleDocs || !moduleDocs.children) { + console.warn( + `${chalk.yellow('Warning:')} No documentation found for ${bundle}` + ); + output = {}; + } else { + output = moduleDocs.children.reduce((result, element) => { + if (parsers[element.kindString]) { + result[element.name] = parsers[element.kindString](element); + } else { + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ + element.name + } of type ${element.type}` + ); + } + return result; + }, {}); + } + + fs.writeFile( + `build/jsons/${bundle}.json`, + JSON.stringify(output, null, 2), + errHandler + ); + } + fs.rm('build/jsons/output', { recursive: true, force: true }, errHandler); + }); +} + +export const buildDocs = async (db) => { + const isBundleModifed = (bundle) => { + if (process.argv[3] === '--force') return true; + + const timestamp = db.get(`bundles.${bundle}`).value() || 0; + return isFolderModified(`src/bundles/${bundle}`, timestamp); + }; + + const bundleNames = Object.keys(modules).filter(isBundleModifed); + if (bundleNames.length === 0) { + console.log('Documentation up to date'); + return null; + } + + console.log( + chalk.greenBright('Building documentation for the following modules:') + ); + console.log( + bundleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') + ); + + return gulp + .src( + bundleNames.map((bundle) => `src/bundles/${bundle}/functions.ts`), + { allowEmpty: true } + ) + .pipe( + typedoc({ + out: 'build/documentation', + json: 'build/docs.json', + tsconfig: 'src/tsconfig.json', + theme: 'typedoc-modules-theme', + readme: `${expandPath()}/README.md`, + excludeInternal: true, + categorizeByGroup: true, + name: 'Source Academy Modules', + }) + ); +}; + +export default async () => { + const db = await getDb(); + await buildDocs(db); + await buildJsons(); +}; diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js new file mode 100644 index 000000000..98dfea618 --- /dev/null +++ b/gulpfile.esm.js/build/index.js @@ -0,0 +1,20 @@ +import { series } from 'gulp'; +import copy from './copyfile'; +import { buildBundles } from './bundles'; +import { buildTabs } from './tabs'; +import { buildDocs } from './docs'; +import { getDb } from './utilities'; + +export default series( + Object.assign( + async () => { + const db = await getDb(); + await Promise.all([buildBundles(db), buildTabs(db), buildDocs(db)]); + }, + { + displayName: 'build', + description: 'Build bundles, tabs and documentation', + } + ), + copy +); diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js new file mode 100644 index 000000000..fc7998f95 --- /dev/null +++ b/gulpfile.esm.js/build/tabs.js @@ -0,0 +1,62 @@ +import gulp from 'gulp'; +import { rollup } from 'gulp-rollup-2'; +import chalk from 'chalk'; +import { + isFolderModified, + getDb, + removeDuplicates, + tabNameToConfig, +} from './utilities'; +import copy from './copyfile'; +import modules from '../../modules.json'; + +export const buildTabs = (db) => { + const isTabModifed = (tabName) => { + const timestamp = db.get(`tabs.${tabName}`).value() || 0; + return isFolderModified(`src/tabs/${tabName}`, timestamp); + }; + + const tabNames = removeDuplicates( + Object.values(modules).flatMap((x) => x.tabs) + ).filter(isTabModifed); + + if (tabNames.length === 0) { + return null; + } + + console.log('Building the following tabs:'); + console.log(tabNames.map(chalk.blue).join('\n')); + + const buildTime = new Date().getTime(); + + const promises = tabNames.map( + (tabName) => + new Promise((resolve, reject) => + // eslint-disable-next-line no-promise-executor-return + gulp + .src(`src/tabs/${tabName}/index.tsx`) + .pipe(rollup(tabNameToConfig(tabName))) + .on('error', reject) + .pipe(gulp.dest('build/tabs/')) + .on('end', () => { + db.set(`tabs.${tabName}`, buildTime).write(); + resolve(); + }) + ) + ); + + return Promise.all(promises); +}; + +export default gulp.series( + Object.assign( + async () => { + const db = await getDb(); + return buildTabs(db); + }, + { + displayName: 'buildTabs', + } + ), + copy +); diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js new file mode 100644 index 000000000..797caf132 --- /dev/null +++ b/gulpfile.esm.js/build/utilities.js @@ -0,0 +1,141 @@ +// /* [Imports] */ +import fs from 'fs'; +import babel from '@rollup/plugin-babel'; +import rollupResolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import chalk from 'chalk'; +import commonJS from 'rollup-plugin-commonjs'; +import filesize from 'rollup-plugin-filesize'; +import injectProcessEnv from 'rollup-plugin-inject-process-env'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import Low from 'lowdb'; +import FileAsync from 'lowdb/adapters/FileAsync'; +import { + BUILD_PATH, + DATABASE_NAME, + NODE_MODULES_PATTERN, + SOURCE_PATTERN, + SUPPRESSED_WARNINGS, +} from './constants'; + +export const defaultConfig = { + onwarn(warning, warn) { + if (SUPPRESSED_WARNINGS.includes(warning.code)) return; + + warn(warning); + }, + plugins: [ + typescript({ + tsconfig: 'src/tsconfig.json', + }), + babel({ + babelHelpers: 'bundled', + extensions: ['.ts', '.tsx'], + include: [SOURCE_PATTERN], + }), + rollupResolve({ + // Source Academy's modules run in a browser environment. The default setting (false) may + // cause compilation issues when using some imported packages. + // https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser + browser: true, + // Tells rollup to look for locally installed modules instead of preferring built-in ones. + // Node's built-in modules include `fs` and `path`, which the jsdom browser environment does + // not have. + // https://github.com/rollup/plugins/tree/master/packages/node-resolve#preferbuiltins + preferBuiltins: false, + }), + commonJS({ + include: NODE_MODULES_PATTERN, + }), + injectProcessEnv({ + NODE_ENV: process.env.NODE_ENV, + }), + + filesize({ + showMinifiedSize: false, + showGzippedSize: false, + }), + ], +}; + +export function tabNameToConfig(tabName) { + console.log(chalk.greenBright('Configured module tabs:')); + console.log(`• ${chalk.blueBright(tabName)}`); + + return { + ...defaultConfig, + + // input: `${tabNameToSourceFolder(tabName)}index.tsx`, + output: { + file: `${BUILD_PATH}tabs/${tabName}.js`, + format: 'iife', + + globals: { + react: 'React', + 'react-dom': 'ReactDom', + }, + }, + external: ['react', 'react-dom'], + }; +} + +export function bundleNameToConfig(bundleName) { + return { + ...defaultConfig, + + // input: `${bundleNameToSourceFolder(bundleName)}index.ts`, + output: { + file: `${BUILD_PATH}bundles/${bundleName}.js`, + format: 'iife', + }, + }; +} +// Function takes in relative paths, for cleaner logging +export function isFolderModified(relativeFolderPath, storedTimestamp) { + function toFullPath(rootRelativePath) { + return join(process.cwd(), rootRelativePath); + } + + let fullFolderPath = toFullPath(relativeFolderPath); + + let contents = fs.readdirSync(fullFolderPath); + for (let content of contents) { + let relativeContentPath = join(relativeFolderPath, content); + let fullContentPath = join(fullFolderPath, content); + + let stats = fs.statSync(fullContentPath); + + // If is folder, recurse. If found something modified, stop early + if ( + stats.isDirectory() && + isFolderModified(relativeContentPath, storedTimestamp) + ) { + return true; + } + + // Is file. Compare timestamps to see if stop early + if (stats.mtimeMs > storedTimestamp) { + console.log(chalk.grey(`• File modified: ${relativeContentPath}`)); + return true; + } + } + + return false; +} + +export function expandPath() { + return dirname(fileURLToPath(import.meta.url)); +} + +export function getDbPath() { + return join(dirname(fileURLToPath(import.meta.url)), `${DATABASE_NAME}.json`); +} + +export function getDb() { + return Low(new FileAsync(getDbPath())); +} + +export function removeDuplicates(arr) { + return [...new Set(arr)]; +} diff --git a/gulpfile.esm.js/index.js b/gulpfile.esm.js/index.js new file mode 100644 index 000000000..e034df635 --- /dev/null +++ b/gulpfile.esm.js/index.js @@ -0,0 +1,7 @@ +/* eslint-disable import/no-named-as-default */ +import build from './build'; +import buildBundles from './build/bundles'; +import buildTabs from './build/tabs'; +import buildDocs from './build/docs'; + +export { build, buildBundles, buildTabs, buildDocs }; diff --git a/package.json b/package.json index 839cf75f0..0e0f2ce4f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint": "Check code for eslint warnings and errors", "lint:fix": "Lint code and fix automatically fixable problems", "build": "Lint code, then build modules and documentation", - "build:rollup": "Transpile modules into the `build/` folder and copy over `modules.json`", + "build:bundles": "Transpile modules into the `build/` folder and copy over `modules.json`", "build:docs": "Generate TypeDocs in the `build/documentation/` folder", "rebuild": "Build only modules whose files have been modified since the last build/rebuild", "serve": "Start the HTTP server to serve all files in `build/`, with the same directory structure", @@ -21,12 +21,13 @@ }, "scripts": { "module": "node ./scripts/templates/app.js", - "lint": "./node_modules/.bin/eslint --ext \".ts, .tsx\" src/", - "lint:fix": "./node_modules/.bin/eslint --ext \".ts, .tsx\" src/ --fix", - "build": "yarn lint && yarn build:rollup && yarn build:docs", - "build:rollup": "./node_modules/.bin/tsc && rollup -c scripts/rollup/rollup.config.js", - "build:docs": "node ./scripts/docs/index.js", - "rebuild": "./node_modules/.bin/tsc && rollup -c scripts/rollup/rollup.config.js --quick", + "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", + "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", + "build": "yarn lint && gulp build", + "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildBundles", + "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildTabs", + "build:docs": "gulp buildDocs", + "rebuild": "./node_modules/.bin/tsc --project src/tsconfig.json && rollup -c scripts/build/rollup.config.js --quick", "serve": "http-server --cors=* -c-1 -p 8022 ./build", "dev": "yarn rebuild && yarn serve", "prepare": "husky install", @@ -39,18 +40,24 @@ "@babel/preset-env": "^7.13.12", "@babel/preset-react": "^7.12.13", "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.18.9", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-node-resolve": "^11.2.0", "@rollup/plugin-typescript": "^8.2.0", "@types/dom-mediacapture-record": "^1.0.11", + "@types/gulp": "^4.0.9", "@types/jest": "^27.4.1", + "@types/lowdb": "^1.0.11", + "@types/merge-stream": "^1.1.2", "@types/node": "^17.0.23", "@types/react": "^17.0.43", "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "babel-jest": "^26.6.3", "chalk": "^4.1.2", + "cross-env": "^7.0.3", "eslint": "^8.12.0", + "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^2.7.1", @@ -59,11 +66,16 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", + "esm": "^3.2.25", "generate-template-files": "^3.0.0", + "gulp": "^4.0.2", + "gulp-rollup-2": "^1.3.1", + "gulp-typedoc": "^3.0.2", "http-server": "^0.12.3", "husky": "5", "jest": "^26.6.3", - "lowdb": "1.0.0", + "lowdb": "^1.0.0", + "merge-stream": "^2.0.0", "prettier": "^2.2.1", "rollup": "^2.41.2", "rollup-plugin-babel": "^4.4.0", @@ -72,6 +84,7 @@ "rollup-plugin-filesize": "^9.1.1", "rollup-plugin-inject-process-env": "^1.3.1", "ts-jest": "^26.5.4", + "ts-node": "^10.9.1", "typedoc": "^0.20.33", "typescript": "^4.2.3", "yarnhook": "^0.5.1" diff --git a/scripts/docs/index.js b/scripts/docs/index.js deleted file mode 100644 index 046060cf6..000000000 --- a/scripts/docs/index.js +++ /dev/null @@ -1,30 +0,0 @@ -const TypeDoc = require('typedoc'); -const paths = require('./paths'); -const modules = require('../../modules.json'); - -async function main() { - const app = new TypeDoc.Application(); - app.options.addReader(new TypeDoc.TSConfigReader()); - app.options.addReader(new TypeDoc.TypeDocReader()); - - app.bootstrap({ - entryPoints: Object.keys(modules).map( - (bundle) => `${paths.root}/src/bundles/${bundle}/functions.ts` - ), - theme: 'typedoc-modules-theme', - readme: `${paths.root}/scripts/docs/README.md`, - excludeInternal: true, - categorizeByGroup: true, - name: 'Source Academy Modules', - }); - - const project = app.convert(); - - if (project) { - const outputDir = 'build/documentation'; - await app.generateDocs(project, outputDir); - await app.generateJson(project, `${outputDir}/documentation.json`); - } -} - -main().catch(console.error); diff --git a/scripts/docs/paths.js b/scripts/docs/paths.js deleted file mode 100644 index 5741e8be8..000000000 --- a/scripts/docs/paths.js +++ /dev/null @@ -1,7 +0,0 @@ -const path = require('path'); - -const root = path.resolve(__dirname, '..', '..'); - -module.exports = { - root, -}; diff --git a/scripts/rollup/rollup.config.js b/scripts/rollup/rollup.config.js deleted file mode 100644 index 43739b48a..000000000 --- a/scripts/rollup/rollup.config.js +++ /dev/null @@ -1,35 +0,0 @@ -/* [Imports] */ -import chalk from 'chalk'; -import { - bundleNamesToConfigs, - getFinalPlugins, - getRollupBundleNames, - tabNamesToConfigs, -} from './utilities.js'; - -/* [Exports] */ -export default function (commandLineArguments) { - let rollupBundleNames = getRollupBundleNames( - Boolean(commandLineArguments.quick) - ); - let { bundleNames, tabNames } = rollupBundleNames; - - // Delete so rollup ignores the custom argument and doesn't log a warning - delete commandLineArguments.quick; - - let bundleConfigs = bundleNamesToConfigs(bundleNames); - let tabConfigs = tabNamesToConfigs(tabNames); - - // Rollup bundle configs, for module bundles and/or module tabs - let rollupBundleConfigs = [...bundleConfigs, ...tabConfigs]; - if (rollupBundleConfigs.length === 0) { - console.log(chalk.yellowBright('(Nothing new to build)\n')); - //NOTE The lack of any config for something real to transpile also means the - // final plugins below don't get the chance to run - process.exit(); - } - - let lastConfig = rollupBundleConfigs[rollupBundleConfigs.length - 1]; - lastConfig.plugins = [...lastConfig.plugins, ...getFinalPlugins()]; - return rollupBundleConfigs; -} diff --git a/scripts/rollup/utilities.js b/scripts/rollup/utilities.js deleted file mode 100644 index 58fc9b25e..000000000 --- a/scripts/rollup/utilities.js +++ /dev/null @@ -1,236 +0,0 @@ -/* [Imports] */ -import babel from '@rollup/plugin-babel'; -import resolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; -import chalk from 'chalk'; -import Low from 'lowdb'; -import FileSync from 'lowdb/adapters/FileSync'; -import { dirname, join } from 'path'; -import commonJS from 'rollup-plugin-commonjs'; -import copy from 'rollup-plugin-copy'; -import filesize from 'rollup-plugin-filesize'; -import injectProcessEnv from 'rollup-plugin-inject-process-env'; -import { fileURLToPath } from 'url'; -import modules from '../../modules.json'; -import fs from 'fs'; -import { - BUILD_PATH, - DATABASE_KEY, - DATABASE_NAME, - MODULES_PATH, - NODE_MODULES_PATTERN, - SOURCE_PATH, - SOURCE_PATTERN, - SUPPRESSED_WARNINGS, -} from './constants.js'; - -/* [Main] */ -let fullDatabasePath = join( - dirname(fileURLToPath(import.meta.url)), - `${DATABASE_NAME}.json` -); -let adapter = new FileSync(fullDatabasePath); -let database = new Low(adapter); - -function getTimestamp() { - return database.get(DATABASE_KEY).value() ?? 0; -} - -function updateTimestamp() { - let newTimestamp = new Date().getTime(); - database.set(DATABASE_KEY, newTimestamp).write(); -} - -// Function takes in relative paths, for cleaner logging -function isFolderModified(relativeFolderPath, storedTimestamp) { - let fullFolderPath = toFullPath(relativeFolderPath); - - let contents = fs.readdirSync(fullFolderPath); - for (let content of contents) { - let relativeContentPath = join(relativeFolderPath, content); - let fullContentPath = join(fullFolderPath, content); - - let stats = fs.statSync(fullContentPath); - - // If is folder, recurse. If found something modified, stop early - if ( - stats.isDirectory() && - isFolderModified(relativeContentPath, storedTimestamp) - ) { - return true; - } - - // Is file. Compare timestamps to see if stop early - if (stats.mtimeMs > storedTimestamp) { - console.log(chalk.grey(`• File modified: ${relativeContentPath}`)); - return true; - } - } - - return false; -} - -function removeDuplicates(array) { - return [...new Set(array)]; -} - -function makeDefaultConfig() { - return { - onwarn(warning, warn) { - if (SUPPRESSED_WARNINGS.includes(warning.code)) return; - - warn(warning); - }, - plugins: [ - typescript(), - babel({ - babelHelpers: 'bundled', - extensions: ['.ts', '.tsx'], - include: [SOURCE_PATTERN], - }), - resolve({ - // Source Academy's modules run in a browser environment. The default setting (false) may - // cause compilation issues when using some imported packages. - // https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser - browser: true, - // Tells rollup to look for locally installed modules instead of preferring built-in ones. - // Node's built-in modules include `fs` and `path`, which the jsdom browser environment does - // not have. - // https://github.com/rollup/plugins/tree/master/packages/node-resolve#preferbuiltins - preferBuiltins: false, - }), - commonJS({ - include: NODE_MODULES_PATTERN, - }), - injectProcessEnv({ - NODE_ENV: process.env.NODE_ENV, - }), - - filesize({ - showMinifiedSize: false, - showGzippedSize: false, - }), - ], - }; -} - -function bundleNameToSourceFolder(bundleName) { - // Root relative path - return `${SOURCE_PATH}bundles/${bundleName}/`; -} - -function tabNameToSourceFolder(tabName) { - // Root relative path - return `${SOURCE_PATH}tabs/${tabName}/`; -} - -function toFullPath(rootRelativePath) { - return join(process.cwd(), rootRelativePath); -} - -/* [Exports] */ -export function getRollupBundleNames(skipUnmodified) { - // All module bundles - let moduleNames = Object.keys(modules); - - // Skip modules whose files haven't been modified - console.log(''); - if (skipUnmodified) { - let storedTimestamp = getTimestamp(); - console.log( - chalk.grey( - `Quick rebuild mode (newer than ${new Date( - storedTimestamp - ).toLocaleString()}):` - ) - ); - - moduleNames = moduleNames.filter((moduleName) => { - // Check module bundle - let relativeBundleFolderPath = bundleNameToSourceFolder(moduleName); - if (isFolderModified(relativeBundleFolderPath, storedTimestamp)) - return true; - - // Check each module tab - for (let tabName of modules[moduleName].tabs) { - let relativeTabFolderPath = tabNameToSourceFolder(tabName); - if (isFolderModified(relativeTabFolderPath, storedTimestamp)) - return true; - } - - return false; - }); - } - - // All module tabs - let tabNames = moduleNames.flatMap((moduleName) => modules[moduleName].tabs); - tabNames = removeDuplicates(tabNames); - - return { - bundleNames: moduleNames, - tabNames, - }; -} - -export function bundleNamesToConfigs(names) { - let defaultConfig = makeDefaultConfig(); - - console.log(chalk.greenBright('Configured module bundles:')); - let configs = names.map((bundleName) => { - console.log(`• ${chalk.blueBright(bundleName)}`); - - return { - ...defaultConfig, - - input: `${bundleNameToSourceFolder(bundleName)}index.ts`, - output: { - file: `${BUILD_PATH}bundles/${bundleName}.js`, - format: 'iife', - }, - }; - }); - - return configs; -} - -export function tabNamesToConfigs(names) { - let defaultConfig = makeDefaultConfig(); - - console.log(chalk.greenBright('Configured module tabs:')); - let configs = names.map((tabName) => { - console.log(`• ${chalk.blueBright(tabName)}`); - - return { - ...defaultConfig, - - input: `${tabNameToSourceFolder(tabName)}index.tsx`, - output: { - file: `${BUILD_PATH}tabs/${tabName}.js`, - format: 'iife', - - globals: { - react: 'React', - 'react-dom': 'ReactDom', - }, - }, - external: ['react', 'react-dom'], - }; - }); - - return configs; -} - -export function getFinalPlugins() { - // Run these only once, at the end - return [ - copy({ - targets: [{ src: MODULES_PATH, dest: BUILD_PATH }], - }), - { - name: 'lowdb-timestamp', - buildEnd(error) { - if (error === undefined) updateTimestamp(); - }, - }, - ]; -} diff --git a/scripts/templates/app.js b/scripts/templates/app.js deleted file mode 100644 index b1d45f3d6..000000000 --- a/scripts/templates/app.js +++ /dev/null @@ -1,29 +0,0 @@ -const print = require('./print'); -const modules = require('./module'); -const tabs = require('./tab'); - -async function main() { - try { - const mode = await askMode(); - if (mode === 'module') await modules.addNew(); - else if (mode === 'tab') await tabs.addNew(); - } catch (error) { - print.error(`ERROR: ${error.message}`); - print.info('Terminating module app...'); - } finally { - print.rl.close(); - } -} - -async function askMode() { - const mode = await print.askQuestion( - 'What would you like to create? (module/tab)' - ); - if (mode !== 'module' && mode !== 'tab') { - print.warn("Please answer with only 'module' or 'tab'."); - return askMode(); - } - return mode; -} - -main(); diff --git a/scripts/templates/module.js b/scripts/templates/module.js deleted file mode 100644 index 33bb61871..000000000 --- a/scripts/templates/module.js +++ /dev/null @@ -1,43 +0,0 @@ -const fs = require('fs').promises; -const paths = require('./paths'); -const print = require('./print'); -const utilities = require('./utilities'); -const manifest = require('../../modules.json'); - -function check(moduleName) { - return Object.keys(manifest).includes(moduleName); -} - -async function askModuleName() { - const name = await print.askQuestion( - 'What is the name of your new module? (eg. binary_tree)' - ); - if (utilities.isSnakeCase(name) === false) { - print.warn('Module names must be in snake case. (eg. binary_tree)'); - return askModuleName(); - } - if (check(name) === true) { - print.warn('A module with the same name already exists.'); - return askModuleName(); - } - return name; -} - -async function addNew() { - const moduleName = await askModuleName(); - const bundleDestination = `${paths.root}/src/bundles/${moduleName}`; - await fs.mkdir(bundleDestination, { recursive: true }); - await fs.copyFile(paths.bundleTemplate, `${bundleDestination}/index.ts`); - await fs.writeFile( - paths.manifest, - JSON.stringify({ ...manifest, [moduleName]: { tabs: [] } }, null, 2) - ); - print.success( - `Bundle for module ${moduleName} created at ${bundleDestination}.` - ); -} - -module.exports = { - addNew, - check, -}; diff --git a/scripts/templates/paths.js b/scripts/templates/paths.js deleted file mode 100644 index e7d7bfb8a..000000000 --- a/scripts/templates/paths.js +++ /dev/null @@ -1,19 +0,0 @@ -const path = require('path'); - -const root = path.resolve(__dirname, '..', '..'); -const manifest = path.resolve(root, 'modules.json'); -const bundleTemplate = path.resolve( - root, - './scripts/templates/templates/__bundle__.ts' -); -const tabTemplate = path.resolve( - root, - './scripts/templates/templates/__tab__.tsx' -); - -module.exports = { - root, - manifest, - bundleTemplate, - tabTemplate, -}; diff --git a/scripts/templates/print.js b/scripts/templates/print.js deleted file mode 100644 index f77643d26..000000000 --- a/scripts/templates/print.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable no-console */ -const chalk = require('chalk'); -const readline = require('readline'); - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}); - -function info(...args) { - return console.log(...args.map((string) => chalk.grey(string))); -} - -function error(...args) { - return console.log(...args.map((string) => chalk.red(string))); -} - -function warn(...args) { - return console.log(...args.map((string) => chalk.yellow(string))); -} - -function success(...args) { - return console.log(...args.map((string) => chalk.green(string))); -} - -function askQuestion(question) { - return new Promise((resolve) => { - rl.question(chalk.blueBright(`${question}\n`), resolve); - }); -} - -module.exports = { - rl, - info, - error, - warn, - success, - askQuestion, -}; diff --git a/scripts/templates/tab.js b/scripts/templates/tab.js deleted file mode 100644 index 8569a9f1f..000000000 --- a/scripts/templates/tab.js +++ /dev/null @@ -1,68 +0,0 @@ -const fs = require('fs').promises; -const paths = require('./paths'); -const print = require('./print'); -const utilities = require('./utilities'); -const modules = require('./module'); -const manifest = require('../../modules.json'); - -const existingTabs = Object.keys(manifest).reduce( - (accumulator, current) => accumulator.concat(manifest[current].tabs), - [] -); - -function check(tabName) { - return existingTabs.includes(tabName); -} - -async function askModuleName() { - const name = await print.askQuestion('Add a new tab to which module?'); - if (modules.check(name) === false) { - print.warn(`Module ${name} does not exist.`); - return askModuleName(); - } - return name; -} - -async function askTabName() { - const name = await print.askQuestion( - 'What is the name of your new tab? (eg. BinaryTree)' - ); - if (utilities.isPascalCase(name) === false) { - print.warn('Tab names must be in pascal case. (eg. BinaryTree)'); - return askTabName(); - } - if (check(name)) { - print.warn('A tab with the same name already exists.'); - return askTabName(); - } - return name; -} - -async function addNew() { - const moduleName = await askModuleName(); - const tabName = await askTabName(); - - // Copy module tab template into correct destination and show success message - const tabDestination = `${paths.root}/src/tabs/${tabName}`; - await fs.mkdir(tabDestination, { recursive: true }); - await fs.copyFile(paths.tabTemplate, `${tabDestination}/index.tsx`); - await fs.writeFile( - paths.manifest, - JSON.stringify( - { - ...manifest, - [moduleName]: { tabs: [...manifest[moduleName].tabs, tabName] }, - }, - null, - 2 - ) - ); - print.success( - `Tab ${tabName} for module ${moduleName} created at ${tabDestination}.` - ); -} - -module.exports = { - addNew, - check, -}; diff --git a/scripts/templates/templates/__bundle__.ts b/scripts/templates/templates/__bundle__.ts deleted file mode 100644 index 3394a2428..000000000 --- a/scripts/templates/templates/__bundle__.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * A single sentence summarising the module (this sentence is displayed larger). - * - * Sentences describing the module. More sentences about the module. - * - * @module module_name - * @author Author Name - * @author Author Name - */ - -import { - ModuleContexts, - ModuleParams, -} from '../../../src/typings/type_helpers.js'; - -/** - * Sample function. Increments a number by 1. - * - * @param x The number to be incremented. - * @returns The incremented value of the number. - */ -function sample_function(x: number): number { - return ++x; -} - -//NOTE Remove the underscores before the parameter names if you will be using -// them. These parameters are passed in over on the frontend, and can later be -// accessed again in your module's tab via the DebuggerContext it gets passed -export default ( - _moduleParams: ModuleParams, - _moduleContexts: ModuleContexts -) => ({ - sample_function, -}); diff --git a/scripts/templates/templates/__tab__.tsx b/scripts/templates/templates/__tab__.tsx deleted file mode 100644 index faa7ff3cd..000000000 --- a/scripts/templates/templates/__tab__.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; - -/** - * - * @author - * @author - */ - -/** - * React Component props for the Tab. - */ -type Props = { - children?: never; - className?: never; - context?: any; -}; - -/** - * React Component state for the Tab. - */ -type State = { - counter: number; -}; - -/** - * The main React Component of the Tab. - */ -class Repeat extends React.Component { - constructor(props) { - super(props); - this.state = { - counter: 0, - }; - } - - public render() { - const { counter } = this.state; - return ( -
    This is spawned from the repeat package. Counter is {counter}
    - ); - } -} - -export default { - /** - * This function will be called to determine if the component will be - * rendered. Currently spawns when the result in the REPL is "test". - * @param {DebuggerContext} context - * @returns {boolean} - */ - toSpawn: (context: any) => context.result.value === 'test', - - /** - * This function will be called to render the module tab in the side contents - * on Source Academy frontend. - * @param {DebuggerContext} context - */ - body: (context: any) => , - - /** - * The Tab's icon tooltip in the side contents on Source Academy frontend. - */ - label: 'Sample Tab', - - /** - * BlueprintJS IconName element's name, used to render the icon which will be - * displayed in the side contents panel. - * @see https://blueprintjs.com/docs/#icons - */ - iconName: 'build', -}; diff --git a/scripts/templates/utilities.js b/scripts/templates/utilities.js deleted file mode 100644 index 05d1e6dee..000000000 --- a/scripts/templates/utilities.js +++ /dev/null @@ -1,15 +0,0 @@ -const snakeCaseRegex = /\b[a-z]+(?:_[a-z]+)*\b/u; -const pascalCaseRegex = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u; - -function isSnakeCase(string) { - return snakeCaseRegex.test(string); -} - -function isPascalCase(string) { - return pascalCaseRegex.test(string); -} - -module.exports = { - isSnakeCase, - isPascalCase, -}; diff --git a/.eslintrc.js b/src/.eslintrc.js similarity index 97% rename from .eslintrc.js rename to src/.eslintrc.js index 8b23de9d5..3036c21df 100644 --- a/.eslintrc.js +++ b/src/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: ['./.eslintrc.base.js', 'plugin:prettier/recommended'], + extends: ['../.eslintrc.base.js', 'plugin:prettier/recommended'], root: true, parserOptions: { @@ -60,6 +60,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { project: './tsconfig.json', + tsconfigRootDir: __dirname, }, rules: { diff --git a/src/bundles/curve/index.ts b/src/bundles/curve/index.ts index b8fb3dad8..bf3b7d78e 100644 --- a/src/bundles/curve/index.ts +++ b/src/bundles/curve/index.ts @@ -53,6 +53,9 @@ export default function curves( // Update the module's global context let moduleContext = moduleContexts.get('curve'); + // Probably can edit this because modules can only be loaded once + // Otherwise loading the module twice just overwrites the existing context + // thing if (!moduleContext) { moduleContext = { tabs: [], diff --git a/tsconfig.json b/src/tsconfig.json similarity index 96% rename from tsconfig.json rename to src/tsconfig.json index 572648eab..aa9f62456 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -31,7 +31,7 @@ "noImplicitAny": false, }, /* Specifies an array of filenames or patterns to include in the program. These filenames are resolved relative to the directory containing the tsconfig.json file. */ - "include": ["src/", "scripts/templates/"], + "include": ["."], /* Specifies an array of filenames or patterns that should be skipped when resolving include. */ - "exclude": [] + "exclude": [], } diff --git a/yarn.lock b/yarn.lock index dda0329af..f6405908f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,6 +966,17 @@ "@babel/helper-validator-option" "^7.12.17" "@babel/plugin-transform-typescript" "^7.13.0" +"@babel/register@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.18.9.tgz#1888b24bc28d5cc41c412feb015e9ff6b96e439c" + integrity sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.5" + source-map-support "^0.5.16" + "@babel/runtime-corejs3@^7.10.2": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86" @@ -1125,6 +1136,13 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@eslint/eslintrc@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" @@ -1354,6 +1372,24 @@ resolved "https://registry.yarnpkg.com/@joeychenofficial/alt-ergo-modified/-/alt-ergo-modified-2.4.0.tgz#27aec0cbed8ab4e2f0dad6feb4f0c9766ac3132f" integrity sha512-58b0K8pNUVZXGbua4IJQ+1K+E+jz3MkhDazZaaeKlD+sOLYR9iTHIbicV/I5K16ivYW6R9lONiT3dz8rMeFJ1w== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jscad/array-utils@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@jscad/array-utils/-/array-utils-2.1.3.tgz#0de2eaaf122c43efc534a948c8d80a65483160e8" @@ -1533,6 +1569,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -1591,6 +1647,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== +"@types/expect@^1.20.4": + version "1.20.4" + resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" + integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== + "@types/fs-extra@^8.0.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" @@ -1598,6 +1659,22 @@ dependencies: "@types/node" "*" +"@types/glob-stream@*": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/glob-stream/-/glob-stream-6.1.1.tgz#c792d8d1514278ff03cad5689aba4c4ab4fbc805" + integrity sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A== + dependencies: + "@types/glob" "*" + "@types/node" "*" + +"@types/glob@*": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -1613,6 +1690,15 @@ dependencies: "@types/node" "*" +"@types/gulp@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.9.tgz#a2f9667bcc26bc72b4899dd16216d6584a12346c" + integrity sha512-zzT+wfQ8uwoXjDhRK9Zkmmk09/fbLLmN/yDHFizJiEKIve85qutOnXcP/TM2sKPBTU+Jc16vfPbOMkORMUBN7Q== + dependencies: + "@types/undertaker" "*" + "@types/vinyl-fs" "*" + chokidar "^3.3.1" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1650,6 +1736,25 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@*": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + +"@types/lowdb@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@types/lowdb/-/lowdb-1.0.11.tgz#d8336a635ea0dbd48a7f6f62fb9fccc5ec358ae3" + integrity sha512-h99VMxvTuz+VsXUVCCJo4dsps4vbkXwvU71TpmxDoiBU24bJ0VBygIHgmMm+UPoQIFihmV6euRik4z8J7XDJWg== + dependencies: + "@types/lodash" "*" + +"@types/merge-stream@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1" + integrity sha512-7faLmaE99g/yX0Y9pF1neh2IUqOf/fXMOWCVzsXjqI1EJ91lrgXmaBKf6bRWM164lLyiHxHt6t/ZO/cIzq61XA== + dependencies: + "@types/node" "*" + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1706,6 +1811,37 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/undertaker-registry@*": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" + integrity sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ== + +"@types/undertaker@*": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.8.tgz#6124a5d78eb6fca84689185229654a6235c601d7" + integrity sha512-gW3PRqCHYpo45XFQHJBhch7L6hytPsIe0QeLujlnFsjHPnXLhJcPdN6a9368d7aIQgH2I/dUTPFBlGeSNA3qOg== + dependencies: + "@types/node" "*" + "@types/undertaker-registry" "*" + async-done "~1.3.2" + +"@types/vinyl-fs@*": + version "2.4.12" + resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-2.4.12.tgz#7b4673d9b4d5a874c8652d10f0f0265479014c8e" + integrity sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw== + dependencies: + "@types/glob-stream" "*" + "@types/node" "*" + "@types/vinyl" "*" + +"@types/vinyl@*": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.6.tgz#b2d134603557a7c3d2b5d3dc23863ea2b5eb29b0" + integrity sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g== + dependencies: + "@types/expect" "^1.20.4" + "@types/node" "*" + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -1833,7 +1969,7 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -1853,6 +1989,11 @@ acorn@^8.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== +acorn@^8.4.1: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -1894,11 +2035,23 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" +ansi-colors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" + integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== + dependencies: + ansi-wrap "^0.1.0" + ansi-colors@^3.2.1: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1906,6 +2059,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw== + dependencies: + ansi-wrap "0.1.0" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -1950,6 +2110,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw== + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -1966,11 +2131,31 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" + integrity sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA== + dependencies: + buffer-equal "^1.0.0" + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== + are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -1979,6 +2164,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2004,11 +2194,25 @@ arr-diff@^4.0.0: resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-flatten@^1.1.0: +arr-filter@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/arr-filter/-/arr-filter-1.1.2.tgz#43fdddd091e8ef11aa4c45d9cdc18e2dff1711ee" + integrity sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA== + dependencies: + make-iterator "^1.0.0" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== +arr-map@^2.0.0, arr-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/arr-map/-/arr-map-2.0.2.tgz#3a77345ffc1cf35e2a91825601f9e58f2e24cac4" + integrity sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw== + dependencies: + make-iterator "^1.0.0" + arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -2019,6 +2223,11 @@ array-differ@^1.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= +array-each@^1.0.0, array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" + integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA== + array-includes@^3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" @@ -2041,6 +2250,35 @@ array-includes@^3.1.4: get-intrinsic "^1.1.1" is-string "^1.0.7" +array-initial@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" + integrity sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw== + dependencies: + array-slice "^1.0.0" + is-number "^4.0.0" + +array-last@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" + integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== + dependencies: + is-number "^4.0.0" + +array-slice@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" + integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== + +array-sort@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" + integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -2118,11 +2356,28 @@ astring@^1.4.3: resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.1.tgz#a91c4afd4af3523e11f31242a3d5d9af62bb6cc6" integrity sha512-Aj3mbwVzj7Vve4I/v2JYOPFkCGM2YS7OqQTNSxmUR+LECRpokuPgAYghePgr6SALDo5bD5DlfbSaYjOzGJZOLQ== +async-done@^1.2.0, async-done@^1.2.2, async-done@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" + integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.2" + process-nextick-args "^2.0.0" + stream-exhaust "^1.0.1" + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +async-settle@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" + integrity sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw== + dependencies: + async-done "^1.2.2" + async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -2257,6 +2512,21 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" +bach@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" + integrity sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg== + dependencies: + arr-filter "^1.1.1" + arr-flatten "^1.0.1" + arr-map "^2.0.0" + array-each "^1.0.0" + array-initial "^1.0.0" + array-last "^1.1.1" + async-done "^1.2.2" + async-settle "^1.0.0" + now-and-later "^2.0.0" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -2405,6 +2675,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + integrity sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ== + buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2479,6 +2754,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg== + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2541,6 +2821,40 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chokidar@^2.0.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chokidar@^3.3.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^3.4.0: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" @@ -2606,6 +2920,15 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w== + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -2615,6 +2938,39 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag== + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + +cloneable-readable@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" + integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -2630,6 +2986,15 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== +collection-map@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-map/-/collection-map-1.0.0.tgz#aea0f06f8d26c780c2b75494385544b2255af18c" + integrity sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA== + dependencies: + arr-map "^2.0.2" + for-own "^1.0.0" + make-iterator "^1.0.0" + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2662,6 +3027,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colorette@^1.1.0, colorette@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" @@ -2689,6 +3059,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -2699,6 +3074,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +concat-stream@^1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + confusing-browser-globals@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" @@ -2716,11 +3101,26 @@ convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" +convert-source-map@^1.5.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +copy-props@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.5.tgz#03cf9ae328d4ebb36f8f1d804448a6af9ee3f2d2" + integrity sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw== + dependencies: + each-props "^1.3.2" + is-plain-object "^5.0.0" + core-js-compat@^3.8.1, core-js-compat@^3.9.0: version "3.9.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455" @@ -2744,6 +3144,18 @@ corser@^2.0.1: resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -2755,7 +3167,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2786,6 +3198,14 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -2840,7 +3260,7 @@ debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -decamelize@^1.2.0: +decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -2889,6 +3309,18 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + +default-resolution@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" + integrity sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -2946,6 +3378,11 @@ depd@^1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -2966,6 +3403,11 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3011,11 +3453,29 @@ duplexer@0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= -duplexer@^0.1.2: +duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +each-props@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333" + integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA== + dependencies: + is-plain-object "^2.0.1" + object.defaults "^1.1.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3071,7 +3531,7 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -3102,7 +3562,7 @@ errno@^0.1.2: dependencies: prr "~1.0.1" -error-ex@^1.3.1: +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3166,6 +3626,42 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.1, es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +es6-weak-map@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3215,6 +3711,15 @@ eslint-config-airbnb-typescript@^17.0.0: dependencies: eslint-config-airbnb-base "^15.0.0" +eslint-config-airbnb@^19.0.4: + version "19.0.4" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3" + integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== + dependencies: + eslint-config-airbnb-base "^15.0.0" + object.assign "^4.1.2" + object.entries "^1.1.5" + eslint-config-prettier@^8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" @@ -3395,6 +3900,11 @@ eslint@^8.12.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^9.3.1: version "9.3.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" @@ -3453,6 +3963,19 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-stream@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65" + integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + eventemitter3@^4.0.0, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3514,6 +4037,13 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== + dependencies: + homedir-polyfill "^1.0.1" + expect@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" @@ -3526,6 +4056,13 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +ext@^1.1.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" + integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== + dependencies: + type "^2.5.0" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3541,7 +4078,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3570,6 +4107,16 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= +fancy-log@^1.3.2, fancy-log@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3608,6 +4155,11 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-levenshtein@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" + integrity sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw== + fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -3661,11 +4213,28 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-cache-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ= +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA== + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -3673,6 +4242,13 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -3681,6 +4257,42 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +findup-sync@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + integrity sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g== + dependencies: + detect-file "^1.0.0" + is-glob "^3.1.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +fined@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" + integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +flagged-respawn@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" + integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -3694,16 +4306,31 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== +flush-write-stream@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + follow-redirects@^1.0.0: version "1.13.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== -for-in@^1.0.2: +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg== + dependencies: + for-in "^1.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -3725,6 +4352,11 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" +from@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -3756,6 +4388,14 @@ fs-minipass@^2.0.0, fs-minipass@^2.1.0: dependencies: minipass "^3.0.0" +fs-mkdirp-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" + integrity sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ== + dependencies: + graceful-fs "^4.1.11" + through2 "^2.0.3" + fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -3766,7 +4406,15 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.1.2, fsevents@~2.3.1: +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fsevents@^2.1.2, fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -3813,6 +4461,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3912,7 +4565,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -3926,6 +4579,35 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + integrity sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw== + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + +glob-watcher@^5.0.3: + version "5.0.5" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.5.tgz#aa6bce648332924d9a8489be41e3e5c52d4186dc" + integrity sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw== + dependencies: + anymatch "^2.0.0" + async-done "^1.2.0" + chokidar "^2.0.0" + is-negated-glob "^1.0.0" + just-debounce "^1.0.0" + normalize-path "^3.0.0" + object.defaults "^1.1.0" + glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -3950,6 +4632,26 @@ glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4007,6 +4709,13 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +glogg@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" + integrity sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA== + dependencies: + sparkles "^1.0.0" + glsl-tokenizer@^2.0.2: version "2.1.5" resolved "https://registry.yarnpkg.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz#1c2e78c16589933c274ba278d0a63b370c5fee1a" @@ -4030,16 +4739,16 @@ gpu.js@^2.10.4: gpu-mock.js "^1.3.0" webgpu "^0.1.16" +graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.3: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + graceful-fs@^4.1.11, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -graceful-fs@^4.1.3: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -4050,6 +4759,70 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== +gulp-cli@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" + integrity sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A== + dependencies: + ansi-colors "^1.0.1" + archy "^1.0.0" + array-sort "^1.0.0" + color-support "^1.1.3" + concat-stream "^1.6.0" + copy-props "^2.0.1" + fancy-log "^1.3.2" + gulplog "^1.0.0" + interpret "^1.4.0" + isobject "^3.0.1" + liftoff "^3.1.0" + matchdep "^2.0.0" + mute-stdout "^1.0.0" + pretty-hrtime "^1.0.0" + replace-homedir "^1.0.0" + semver-greatest-satisfied-range "^1.1.0" + v8flags "^3.2.0" + yargs "^7.1.0" + +gulp-rollup-2@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/gulp-rollup-2/-/gulp-rollup-2-1.3.1.tgz#bbe1a8725ccaa4fc270627b298d1072d960fbe0c" + integrity sha512-sKkHsbhmcFBorQ3AHUME8Kifj3Ud7d6BwUMntCigolJQxzbXH8Ja688WRc3dolSlcp31cIWTtJf9r+090gC+Ew== + dependencies: + njfs "^1.2.5" + object-hash "^2.1.1" + rollup "^2.41.4" + through2 "^4.0.2" + vinyl "^2.2.1" + vinyl-sourcemaps-apply "^0.2.1" + +gulp-typedoc@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gulp-typedoc/-/gulp-typedoc-3.0.2.tgz#8466dd83d155658526c8180dab2fdd46e010770f" + integrity sha512-f7TVA6/fmZMU1vAskeZbeqBRpHbkFiY49r8xQV8aFIJSoo/Vvw67Tsp9jfUlPAQPC/LYBn/dMg3WmROSbdzskw== + dependencies: + ansi-colors "^4.1.1" + event-stream "^4.0.1" + fancy-log "^1.3.3" + plugin-error "^1.0.1" + semver "^7.3.5" + +gulp@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa" + integrity sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA== + dependencies: + glob-watcher "^5.0.3" + gulp-cli "^2.2.0" + undertaker "^1.2.1" + vinyl-fs "^3.0.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + integrity sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw== + dependencies: + glogg "^1.0.0" + gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -4162,6 +4935,13 @@ he@^1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -4334,7 +5114,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4344,7 +5124,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -4358,16 +5138,29 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -interpret@^1.0.0: +interpret@^1.0.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ== + ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -4456,6 +5249,13 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -4568,6 +5368,11 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug== + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -4590,6 +5395,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -4614,7 +5424,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -4626,6 +5436,11 @@ is-plain-object@^3.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-potential-custom-element-name@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" @@ -4659,6 +5474,13 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + is-shared-array-buffer@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -4697,8 +5519,25 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-utf8@^0.2.0, is-utf8@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== + +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA== is-weakref@^1.0.2: version "1.0.2" @@ -4707,7 +5546,7 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -is-windows@^1.0.2: +is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== @@ -5366,6 +6205,11 @@ junk@^1.0.1: resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592" integrity sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI= +just-debounce@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" + integrity sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -5380,7 +6224,7 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0: +kind-of@^5.0.0, kind-of@^5.0.2: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== @@ -5407,6 +6251,35 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" +last-run@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b" + integrity sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ== + dependencies: + default-resolution "^2.0.0" + es6-weak-map "^2.0.1" + +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw== + dependencies: + invert-kv "^1.0.0" + +lead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" + integrity sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow== + dependencies: + flush-write-stream "^1.0.2" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -5428,11 +6301,36 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +liftoff@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" + integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== + dependencies: + extend "^3.0.0" + findup-sync "^3.0.0" + fined "^1.0.1" + flagged-respawn "^1.0.0" + is-plain-object "^2.0.4" + object.map "^1.0.0" + rechoir "^0.6.2" + resolve "^1.1.7" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A== + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5441,6 +6339,14 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -5470,7 +6376,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lowdb@1.0.0: +lowdb@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== @@ -5512,7 +6418,7 @@ magic-string@^0.25.2, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" -make-dir@^2.1.0: +make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -5527,7 +6433,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -5553,6 +6459,13 @@ make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" +make-iterator@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" + integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== + dependencies: + kind-of "^6.0.2" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -5560,11 +6473,16 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-cache@^0.2.2: +map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -5577,6 +6495,16 @@ marked@^2.0.1: resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.1.tgz#5e7ed7009bfa5c95182e4eb696f85e948cefcee3" integrity sha512-5+/fKgMv2hARmMW7DOpykr2iLhl0NgjyELk5yn92iE7z8Se1IS9n3UsFm86hFXIkvMBmVxki8+ckcpjBeyo/hw== +matchdep@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" + integrity sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA== + dependencies: + findup-sync "^2.0.0" + micromatch "^3.0.4" + resolve "^1.4.0" + stack-trace "0.0.10" + maximatch@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/maximatch/-/maximatch-0.1.0.tgz#86cd8d6b04c9f307c05a6b9419906d0360fb13a2" @@ -5597,7 +6525,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -5785,6 +6713,16 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mute-stdout@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" + integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== + +nan@^2.12.1: + version "2.16.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + nan@^2.15.0: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" @@ -5822,11 +6760,21 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +njfs@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/njfs/-/njfs-1.2.5.tgz#082938e9fbf07044572e31c8624a81d9988b517a" + integrity sha512-Vnl1VDjSo7fO3RHAO5N9QrsyWaIpSTxHSwVoQOcepYAx3Lt+2JD/tUwgEJHfh1QqBjyy/H9WS5L79+hL+l99ug== + node-abi@^2.30.1, node-abi@^2.7.0: version "2.30.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" @@ -5894,7 +6842,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.5.0: +normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -5921,6 +6869,13 @@ normalize.css@^8.0.1: resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== +now-and-later@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" + integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== + dependencies: + once "^1.3.2" + npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" @@ -6035,6 +6990,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -6065,7 +7025,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -6075,6 +7035,16 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.defaults@^1.0.0, object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" + integrity sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA== + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -6101,13 +7071,29 @@ object.hasown@^1.1.0: define-properties "^1.1.3" es-abstract "^1.19.1" -object.pick@^1.3.0: +object.map@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" + integrity sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w== + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + +object.pick@^1.2.0, object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" +object.reduce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.reduce/-/object.reduce-1.0.1.tgz#6fe348f2ac7fa0f95ca621226599096825bb03ad" + integrity sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw== + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + object.values@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" @@ -6117,7 +7103,7 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -6167,6 +7153,20 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + integrity sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw== + dependencies: + readable-stream "^2.0.1" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g== + dependencies: + lcid "^1.0.0" + p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -6184,7 +7184,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.2.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -6198,6 +7198,13 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -6254,6 +7261,22 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-filepath@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" + integrity sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q== + dependencies: + is-absolute "^1.0.0" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== + dependencies: + error-ex "^1.2.0" + parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -6264,6 +7287,16 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== + parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -6284,6 +7317,13 @@ path-exists@4.0.0, path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ== + dependencies: + pinkie-promise "^2.0.0" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -6319,6 +7359,27 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== + dependencies: + path-root-regex "^0.1.0" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg== + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -6332,6 +7393,13 @@ path@^0.12.7: process "^0.11.1" util "^0.10.3" +pause-stream@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== + dependencies: + through "~2.3" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -6363,7 +7431,7 @@ pify@^2.0.0, pify@^2.3.0: pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pify@^4.0.1: version "4.0.1" @@ -6389,6 +7457,18 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pirates@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -6396,6 +7476,16 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +plugin-error@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" + integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== + dependencies: + ansi-colors "^1.0.1" + arr-diff "^4.0.0" + arr-union "^3.1.0" + extend-shallow "^3.0.2" + popper.js@^1.14.4, popper.js@^1.16.1: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -6477,7 +7567,12 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" -process-nextick-args@~2.0.0: +pretty-hrtime@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== + +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== @@ -6548,6 +7643,14 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -6556,6 +7659,15 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +pumpify@^1.3.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -6662,6 +7774,14 @@ read-package-json-fast@^2.0.1: json-parse-even-better-errors "^2.3.0" npm-normalize-package-bin "^1.0.1" +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A== + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -6671,6 +7791,15 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ== + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" @@ -6681,7 +7810,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6700,7 +7829,7 @@ read-pkg@^5.2.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.2, readable-stream@^2.0.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -6729,6 +7858,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -6834,7 +7970,24 @@ regl@2.1.0, regl@^2.1.0: resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.0.tgz#7dae71e9ff20f29c4f42f510c70cd92ebb6b657c" integrity sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg== -remove-trailing-separator@^1.0.1: +remove-bom-buffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" + integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== + dependencies: + is-buffer "^1.1.5" + is-utf8 "^0.2.1" + +remove-bom-stream@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" + integrity sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA== + dependencies: + remove-bom-buffer "^3.0.0" + safe-buffer "^5.1.0" + through2 "^2.0.3" + +remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= @@ -6849,6 +8002,20 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + +replace-homedir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c" + integrity sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg== + dependencies: + homedir-polyfill "^1.0.1" + is-absolute "^1.0.0" + remove-trailing-separator "^1.1.0" + replace-string@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/replace-string/-/replace-string-3.1.0.tgz#77a087d88580fbac59851237891aa4b0e283db72" @@ -6901,6 +8068,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -6923,6 +8095,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -6933,6 +8113,13 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-options@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" + integrity sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A== + dependencies: + value-or-function "^3.0.0" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -6946,6 +8133,15 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.2, resolve@^1.17 is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^1.1.7, resolve@^1.4.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.20.0, resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" @@ -7057,6 +8253,13 @@ rollup@^2.41.2: optionalDependencies: fsevents "~2.3.1" +rollup@^2.41.4: + version "2.77.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.2.tgz#6b6075c55f9cc2040a5912e6e062151e42e2c4e3" + integrity sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -7069,7 +8272,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -7126,6 +8329,13 @@ secure-compare@3.0.1: resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM= +semver-greatest-satisfied-range@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" + integrity sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ== + dependencies: + sver-compat "^1.5.0" + "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -7177,6 +8387,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -7334,6 +8551,14 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-support@^0.5.16: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.6, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" @@ -7347,7 +8572,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.5.0, source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -7367,6 +8592,11 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +sparkles@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" + integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== + spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -7400,6 +8630,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -7427,6 +8664,11 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + stack-utils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" @@ -7450,10 +8692,28 @@ stealthy-require@^1.1.1: steno@^0.4.1: version "0.4.4" resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" - integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs= + integrity sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w== dependencies: graceful-fs "^4.1.3" +stream-combiner@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-exhaust@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" + integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -7462,7 +8722,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -7581,6 +8841,13 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g== + dependencies: + is-utf8 "^0.2.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -7638,6 +8905,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +sver-compat@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" + integrity sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg== + dependencies: + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -7712,6 +8987,14 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +through2-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + through2@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" @@ -7727,11 +9010,44 @@ through2@^0.6.3: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" +through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through@2, through@^2.3.8, through@~2.3, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw== + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= +to-absolute-glob@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + integrity sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA== + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -7769,6 +9085,13 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-through@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" + integrity sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q== + dependencies: + through2 "^2.0.3" + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -7809,6 +9132,25 @@ ts-jest@^26.5.4: semver "7.x" yargs-parser "20.x" +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -7892,6 +9234,16 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.5.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.0.tgz#aaff4ac90514e0dc1095b54af70505ef16cf00a2" + integrity sha512-NybX0NBIssNEj1efLf1mqKAtO4Q/Np5mqpa57be81ud7/tNHIXn48FDVXiyGMBF90FfXc5o7RPsuRQrPzgMOMA== + typed-styles@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" @@ -7904,6 +9256,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typedoc-default-themes@^0.12.10: version "0.12.10" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843" @@ -7961,6 +9318,32 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== + +undertaker-registry@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50" + integrity sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw== + +undertaker@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.3.0.tgz#363a6e541f27954d5791d6fa3c1d321666f86d18" + integrity sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg== + dependencies: + arr-flatten "^1.0.1" + arr-map "^2.0.0" + bach "^1.0.0" + collection-map "^1.0.0" + es6-weak-map "^2.0.1" + fast-levenshtein "^1.0.0" + last-run "^1.1.0" + object.defaults "^1.0.0" + object.reduce "^1.0.0" + undertaker-registry "^1.0.0" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -8015,6 +9398,14 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unique-stream@^2.0.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" + integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "^3.0.0" + universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -8082,6 +9473,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -8096,6 +9492,13 @@ v8-to-istanbul@^7.0.0: convert-source-map "^1.6.0" source-map "^0.7.3" +v8flags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" + integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== + dependencies: + homedir-polyfill "^1.0.1" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -8111,6 +9514,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +value-or-function@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" + integrity sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg== + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -8120,6 +9528,61 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl-fs@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" + integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== + dependencies: + fs-mkdirp-stream "^1.0.0" + glob-stream "^6.1.0" + graceful-fs "^4.0.0" + is-valid-glob "^1.0.0" + lazystream "^1.0.0" + lead "^1.0.0" + object.assign "^4.0.4" + pumpify "^1.3.5" + readable-stream "^2.3.3" + remove-bom-buffer "^3.0.0" + remove-bom-stream "^1.2.0" + resolve-options "^1.1.0" + through2 "^2.0.0" + to-through "^2.0.0" + value-or-function "^3.0.0" + vinyl "^2.0.0" + vinyl-sourcemap "^1.1.0" + +vinyl-sourcemap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" + integrity sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA== + dependencies: + append-buffer "^1.0.2" + convert-source-map "^1.5.0" + graceful-fs "^4.1.6" + normalize-path "^2.1.1" + now-and-later "^2.0.0" + remove-bom-buffer "^3.0.0" + vinyl "^2.0.0" + +vinyl-sourcemaps-apply@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + integrity sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw== + dependencies: + source-map "^0.5.1" + +vinyl@^2.0.0, vinyl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" + integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + vscode-textmate@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.3.1.tgz#7ce578a4841a1b114485388ef734cc9e23b45a02" @@ -8200,6 +9663,11 @@ which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + integrity sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ== + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -8210,7 +9678,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== -which@^1.2.9: +which@^1.2.14, which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -8248,6 +9716,14 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw== + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -8303,11 +9779,16 @@ xmlhttprequest-ts@^1.0.1: dependencies: tslib "^1.9.2" -"xtend@>=4.0.0 <4.1.0-0": +"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y18n@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" + integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== + y18n@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" @@ -8336,6 +9817,14 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" + integrity sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA== + dependencies: + camelcase "^3.0.0" + object.assign "^4.1.0" + yargs@15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" @@ -8370,6 +9859,25 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db" + integrity sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA== + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.1" + yarnhook@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/yarnhook/-/yarnhook-0.5.1.tgz#27d47947412bd124874d56aa75725b76b68cc398" @@ -8377,3 +9885,8 @@ yarnhook@^0.5.1: dependencies: execa "^4.0.3" find-parent-dir "^0.3.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From a264f8189cd769a7e07535ca4391b599e1ce12a8 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 13:43:29 +0800 Subject: [PATCH 02/51] Updated error messages --- gulpfile.esm.js/build/{ => docs}/README.md | 0 gulpfile.esm.js/build/docs/index.js | 123 ++++++++++----------- 2 files changed, 60 insertions(+), 63 deletions(-) rename gulpfile.esm.js/build/{ => docs}/README.md (100%) diff --git a/gulpfile.esm.js/build/README.md b/gulpfile.esm.js/build/docs/README.md similarity index 100% rename from gulpfile.esm.js/build/README.md rename to gulpfile.esm.js/build/docs/README.md diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index 9fb7c2dfd..f6a53a240 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -6,6 +6,62 @@ import drawdown from './drawdown'; import { isFolderModified, getDb, expandPath } from '../utilities'; import modules from '../../../modules.json'; +const parsers = { + Variable: (element, bundle) => { + let desc; + if (element.comment && element.comment.shortText) { + desc = drawdown(element.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ + element.name + }` + ); + } + + const typeStr = + element.type.name && element.type.name ? `:${element.type.name}` : ''; + + return `

    ${element.name}${typeStr}

    ${desc}
    `; + }, + Function: (element, bundle) => { + if (!element.signatures || element.signatures[0] === undefined) + throw new Error( + `Error: ${bundle}: Unable to find a signature for function ${element.name}!` + ); + + // In source all functions should only have one signature + const signature = element.signatures[0]; + + // Form the parameter string for the function + let paramStr; + if (!signature.parameters) paramStr = `()`; + else + paramStr = `(${signature.parameters + .map((param) => param.name) + .join(', ')})`; + + // Form the result representation for the function + let resultStr; + if (!signature.type) resultStr = `void`; + else resultStr = signature.type.name; + + let desc; + if (signature.comment && signature.comment.shortText) { + desc = drawdown(signature.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ + element.name + }` + ); + } + + return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; + }, +}; /** * Build the json documentation for the specified modules */ @@ -16,63 +72,6 @@ export async function buildJsons() { if (!fs.existsSync('build/jsons')) fs.mkdirSync(`build/jsons`, {}); - const parsers = { - Variable: (element) => { - let desc; - if (element.comment && element.comment.shortText) { - desc = drawdown(element.comment.shortText); - } else { - desc = element.name; - console.warn( - `${chalk.yellow('Warning:')} ${module}: No description found for ${ - element.name - }` - ); - } - - const typeStr = - element.type.name && element.type.name ? `:${element.type.name}` : ''; - - return `

    ${element.name}${typeStr}

    ${desc}
    `; - }, - Function: (element) => { - if (!element.signatures || element.signatures[0] === undefined) - throw new Error( - `Error: ${module}: Unable to find a signature for function ${element.name}!` - ); - - // In source all functions should only have one signature - const signature = element.signatures[0]; - - // Form the parameter string for the function - let paramStr; - if (!signature.parameters) paramStr = `()`; - else - paramStr = `(${signature.parameters - .map((param) => param.name) - .join(', ')})`; - - // Form the result representation for the function - let resultStr; - if (!signature.type) resultStr = `void`; - else resultStr = signature.type.name; - - let desc; - if (signature.comment && signature.comment.shortText) { - desc = drawdown(signature.comment.shortText); - } else { - desc = element.name; - console.warn( - `${chalk.yellow('Warning:')} ${module}: No description found for ${ - element.name - }` - ); - } - - return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; - }, - }; - // Read from the TypeDoc output and retrieve the JSON relevant to the each module fs.readFile('build/docs.json', 'utf-8', (err, data) => { if (err) throw err; @@ -88,16 +87,15 @@ export async function buildJsons() { for (const bundle of bundles) { const moduleDocs = parsedJSON.find((x) => x.name === bundle); - let output; + const output = {}; if (!moduleDocs || !moduleDocs.children) { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}` ); - output = {}; } else { - output = moduleDocs.children.reduce((result, element) => { + moduleDocs.children.forEach((element) => { if (parsers[element.kindString]) { - result[element.name] = parsers[element.kindString](element); + output[element.name] = parsers[element.kindString](element, bundle); } else { console.warn( `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ @@ -105,8 +103,7 @@ export async function buildJsons() { } of type ${element.type}` ); } - return result; - }, {}); + }); } fs.writeFile( From eb9a8170441622a549bf59a1a23d321edf53f021 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 19:38:17 +0800 Subject: [PATCH 03/51] Removed unnecessary packages --- gulpfile.esm.js/.eslintrc.js | 1 + gulpfile.esm.js/build/bundles.js | 33 ++-- gulpfile.esm.js/build/database.json | 52 +++---- gulpfile.esm.js/build/docs/index.js | 77 ++++----- gulpfile.esm.js/build/tabs.js | 52 ++++--- gulpfile.esm.js/build/utilities.js | 44 ++---- package.json | 3 - yarn.lock | 234 +--------------------------- 8 files changed, 134 insertions(+), 362 deletions(-) diff --git a/gulpfile.esm.js/.eslintrc.js b/gulpfile.esm.js/.eslintrc.js index 9d1e71885..0c13ba514 100644 --- a/gulpfile.esm.js/.eslintrc.js +++ b/gulpfile.esm.js/.eslintrc.js @@ -60,6 +60,7 @@ module.exports = { rules: { 'import/no-extraneous-dependencies': 0, 'no-console': 0, + 'no-continue': 0, 'no-param-reassign': 0, 'no-restricted-syntax': 0, 'prefer-const': 0, diff --git a/gulpfile.esm.js/build/bundles.js b/gulpfile.esm.js/build/bundles.js index 8b3a69171..5f920215f 100644 --- a/gulpfile.esm.js/build/bundles.js +++ b/gulpfile.esm.js/build/bundles.js @@ -1,9 +1,9 @@ import chalk from 'chalk'; import gulp from 'gulp'; -import { rollup } from 'gulp-rollup-2'; +import { rollup } from 'rollup'; import modules from '../../modules.json'; import copy from './copyfile'; -import { bundleNameToConfig, getDb, isFolderModified } from './utilities'; +import { defaultConfig, getDb, isFolderModified } from './utilities'; export const buildBundles = (db) => { const isBundleModifed = (bundle) => { @@ -26,22 +26,21 @@ export const buildBundles = (db) => { const buildTime = new Date().getTime(); - const promises = moduleNames.map( - (bundle) => - new Promise((resolve, reject) => - // eslint-disable-next-line no-promise-executor-return - gulp - .src(`src/bundles/${bundle}/index.ts`) - .pipe(rollup(bundleNameToConfig(bundle))) - .on('error', reject) - .pipe(gulp.dest('build/bundles/')) - .on('end', () => { - db.set(`bundles.${bundle}`, buildTime).write(); - resolve(); - }) - ) - ); + const processBundle = async (bundle) => { + const result = await rollup({ + ...defaultConfig, + input: `src/bundles/${bundle}/index.ts`, + }); + + await result.write({ + file: `build/bundles/${bundle}.js`, + format: 'iife', + }); + + db.set(`bundle.${bundle}`, buildTime).write(); + }; + const promises = moduleNames.map((bundle) => processBundle(bundle)); return Promise.all(promises); }; diff --git a/gulpfile.esm.js/build/database.json b/gulpfile.esm.js/build/database.json index c1a8de200..82d62480b 100644 --- a/gulpfile.esm.js/build/database.json +++ b/gulpfile.esm.js/build/database.json @@ -1,30 +1,30 @@ { - "bundles": { - "copy_gc": 1659718096260, - "mark_sweep": 1659718096260, - "binary_tree": 1659718096260, - "repeat": 1659718096260, - "sound_matrix": 1659718096260, - "scrabble": 1659718096260, - "sound": 1659718096260, - "stereo_sound": 1659718096260, - "pix_n_flix": 1659718096260, - "game": 1659718096260, - "curve": 1659718096260, - "rune": 1659718096260, - "csg": 1659718096260 - }, "tabs": { - "Repeat": 1659715354944, - "Game": 1659715354944, - "CopyGc": 1659715354944, - "Pixnflix": 1659715354944, - "StereoSound": 1659715354944, - "Sound": 1659715354944, - "SoundMatrix": 1659715354944, - "MarkSweep": 1659715354944, - "Csg": 1659715354944, - "Curve": 1659715354944, - "Rune": 1659715354944 + "Game": 1659780674058, + "Repeat": 1659780674058, + "Sound": 1659780674058, + "StereoSound": 1659780674058, + "SoundMatrix": 1659780674058, + "CopyGc": 1659780674058, + "Pixnflix": 1659780674058, + "MarkSweep": 1659780674058, + "Csg": 1659780674058, + "Rune": 1659780674058, + "Curve": 1659780674058 + }, + "bundle": { + "binary_tree": 1659781377620, + "scrabble": 1659781377620, + "pix_n_flix": 1659781377620, + "sound_matrix": 1659781377620, + "sound": 1659781377620, + "stereo_sound": 1659781377620, + "copy_gc": 1659781377620, + "mark_sweep": 1659781377620, + "repeat": 1659781377620, + "game": 1659781377620, + "rune": 1659781377620, + "curve": 1659781377620, + "csg": 1659781377620 } } \ No newline at end of file diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index f6a53a240..e4d9e9209 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -1,9 +1,8 @@ import fs from 'fs'; -import gulp from 'gulp'; -import typedoc from 'gulp-typedoc'; +import * as typedoc from 'typedoc'; import chalk from 'chalk'; import drawdown from './drawdown'; -import { isFolderModified, getDb, expandPath } from '../utilities'; +import { isFolderModified, getDb, cjsDirname } from '../utilities'; import modules from '../../../modules.json'; const parsers = { @@ -39,7 +38,10 @@ const parsers = { if (!signature.parameters) paramStr = `()`; else paramStr = `(${signature.parameters - .map((param) => param.name) + .map((param) => { + const typeStr = param.type ? param.type.name : 'unknown'; + return `${param.name}: ${typeStr}`; + }) .join(', ')})`; // Form the result representation for the function @@ -92,20 +94,21 @@ export async function buildJsons() { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}` ); - } else { - moduleDocs.children.forEach((element) => { - if (parsers[element.kindString]) { - output[element.name] = parsers[element.kindString](element, bundle); - } else { - console.warn( - `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ - element.name - } of type ${element.type}` - ); - } - }); + continue; } + moduleDocs.children.forEach((element) => { + if (parsers[element.kindString]) { + output[element.name] = parsers[element.kindString](element, bundle); + } else { + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ + element.name + } of type ${element.type}` + ); + } + }); + fs.writeFile( `build/jsons/${bundle}.json`, JSON.stringify(output, null, 2), @@ -127,33 +130,37 @@ export const buildDocs = async (db) => { const bundleNames = Object.keys(modules).filter(isBundleModifed); if (bundleNames.length === 0) { console.log('Documentation up to date'); - return null; + return; } console.log( - chalk.greenBright('Building documentation for the following modules:') + chalk.greenBright('Building documentation for the following bundles:') ); console.log( bundleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') ); - return gulp - .src( - bundleNames.map((bundle) => `src/bundles/${bundle}/functions.ts`), - { allowEmpty: true } - ) - .pipe( - typedoc({ - out: 'build/documentation', - json: 'build/docs.json', - tsconfig: 'src/tsconfig.json', - theme: 'typedoc-modules-theme', - readme: `${expandPath()}/README.md`, - excludeInternal: true, - categorizeByGroup: true, - name: 'Source Academy Modules', - }) - ); + const app = new typedoc.Application(); + app.options.addReader(new typedoc.TSConfigReader()); + app.options.addReader(new typedoc.TypeDocReader()); + + app.bootstrap({ + entryPoints: bundleNames.map( + (bundle) => `src/bundles/${bundle}/functions.ts` + ), + tsconfig: 'src/tsconfig.json', + theme: 'typedoc-modules-theme', + readme: `${cjsDirname()}/README.md`, + excludeInternal: true, + categorizeByGroup: true, + name: 'Source Academy Modules', + }); + + const project = app.convert(); + if (project) { + await app.generateDocs(project, 'build/documentation'); + await app.generateJson(project, 'build/docs.json'); + } }; export default async () => { diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js index fc7998f95..a7df26d90 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/gulpfile.esm.js/build/tabs.js @@ -1,17 +1,19 @@ import gulp from 'gulp'; -import { rollup } from 'gulp-rollup-2'; +import { rollup } from 'rollup'; import chalk from 'chalk'; +import fs from 'fs'; import { isFolderModified, getDb, removeDuplicates, - tabNameToConfig, + defaultConfig, } from './utilities'; import copy from './copyfile'; import modules from '../../modules.json'; export const buildTabs = (db) => { const isTabModifed = (tabName) => { + if (process.argv[3] === '--force') return true; const timestamp = db.get(`tabs.${tabName}`).value() || 0; return isFolderModified(`src/tabs/${tabName}`, timestamp); }; @@ -24,26 +26,36 @@ export const buildTabs = (db) => { return null; } - console.log('Building the following tabs:'); - console.log(tabNames.map(chalk.blue).join('\n')); + console.log(chalk.greenBright('Building the following tabs:')); + console.log(tabNames.map((x) => `• ${chalk.blue(x)}`).join('\n')); const buildTime = new Date().getTime(); - const promises = tabNames.map( - (tabName) => - new Promise((resolve, reject) => - // eslint-disable-next-line no-promise-executor-return - gulp - .src(`src/tabs/${tabName}/index.tsx`) - .pipe(rollup(tabNameToConfig(tabName))) - .on('error', reject) - .pipe(gulp.dest('build/tabs/')) - .on('end', () => { - db.set(`tabs.${tabName}`, buildTime).write(); - resolve(); - }) - ) - ); + const processTab = async (tabName) => { + const result = await rollup({ + ...defaultConfig, + input: `src/tabs/${tabName}/index.tsx`, + external: ['react', 'react-dom'], + }); + + const tabFile = `build/tabs/${tabName}.js`; + await result.write({ + file: tabFile, + format: 'iife', + globals: { + react: 'React', + 'react-dom': 'ReactDom', + }, + }); + + const rawTab = fs.readFileSync(tabFile, 'utf-8'); + const lastBracket = rawTab.lastIndexOf('('); + fs.writeFileSync(tabFile, `${rawTab.substring(0, lastBracket)})`); + + db.set(`tabs.${tabName}`, buildTime).write(); + }; + + const promises = tabNames.map(processTab); return Promise.all(promises); }; @@ -52,7 +64,7 @@ export default gulp.series( Object.assign( async () => { const db = await getDb(); - return buildTabs(db); + await buildTabs(db); }, { displayName: 'buildTabs', diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index 797caf132..f687dfe9a 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -12,7 +12,6 @@ import { fileURLToPath } from 'url'; import Low from 'lowdb'; import FileAsync from 'lowdb/adapters/FileAsync'; import { - BUILD_PATH, DATABASE_NAME, NODE_MODULES_PATTERN, SOURCE_PATTERN, @@ -59,38 +58,6 @@ export const defaultConfig = { ], }; -export function tabNameToConfig(tabName) { - console.log(chalk.greenBright('Configured module tabs:')); - console.log(`• ${chalk.blueBright(tabName)}`); - - return { - ...defaultConfig, - - // input: `${tabNameToSourceFolder(tabName)}index.tsx`, - output: { - file: `${BUILD_PATH}tabs/${tabName}.js`, - format: 'iife', - - globals: { - react: 'React', - 'react-dom': 'ReactDom', - }, - }, - external: ['react', 'react-dom'], - }; -} - -export function bundleNameToConfig(bundleName) { - return { - ...defaultConfig, - - // input: `${bundleNameToSourceFolder(bundleName)}index.ts`, - output: { - file: `${BUILD_PATH}bundles/${bundleName}.js`, - format: 'iife', - }, - }; -} // Function takes in relative paths, for cleaner logging export function isFolderModified(relativeFolderPath, storedTimestamp) { function toFullPath(rootRelativePath) { @@ -124,14 +91,23 @@ export function isFolderModified(relativeFolderPath, storedTimestamp) { return false; } -export function expandPath() { +/** + * Function to replicate the functionality of `__dirname` in CJS code + */ +export function cjsDirname() { return dirname(fileURLToPath(import.meta.url)); } +/** + * Get the path to the database file + */ export function getDbPath() { return join(dirname(fileURLToPath(import.meta.url)), `${DATABASE_NAME}.json`); } +/** + * Get a new Lowdb instance + */ export function getDb() { return Low(new FileAsync(getDbPath())); } diff --git a/package.json b/package.json index 0e0f2ce4f..c63207dac 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@babel/preset-env": "^7.13.12", "@babel/preset-react": "^7.12.13", "@babel/preset-typescript": "^7.13.0", - "@babel/register": "^7.18.9", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-node-resolve": "^11.2.0", "@rollup/plugin-typescript": "^8.2.0", @@ -69,7 +68,6 @@ "esm": "^3.2.25", "generate-template-files": "^3.0.0", "gulp": "^4.0.2", - "gulp-rollup-2": "^1.3.1", "gulp-typedoc": "^3.0.2", "http-server": "^0.12.3", "husky": "5", @@ -84,7 +82,6 @@ "rollup-plugin-filesize": "^9.1.1", "rollup-plugin-inject-process-env": "^1.3.1", "ts-jest": "^26.5.4", - "ts-node": "^10.9.1", "typedoc": "^0.20.33", "typescript": "^4.2.3", "yarnhook": "^0.5.1" diff --git a/yarn.lock b/yarn.lock index f6405908f..c681c6d84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,17 +966,6 @@ "@babel/helper-validator-option" "^7.12.17" "@babel/plugin-transform-typescript" "^7.13.0" -"@babel/register@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.18.9.tgz#1888b24bc28d5cc41c412feb015e9ff6b96e439c" - integrity sha512-ZlbnXDcNYHMR25ITwwNKT88JiaukkdVj/nG7r3wnuXkOTHc60Uy05PwMCPre0hSkY68E6zK3xz+vUJSP2jWmcw== - dependencies: - clone-deep "^4.0.1" - find-cache-dir "^2.0.0" - make-dir "^2.1.0" - pirates "^4.0.5" - source-map-support "^0.5.16" - "@babel/runtime-corejs3@^7.10.2": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86" @@ -1136,13 +1125,6 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - "@eslint/eslintrc@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" @@ -1372,24 +1354,6 @@ resolved "https://registry.yarnpkg.com/@joeychenofficial/alt-ergo-modified/-/alt-ergo-modified-2.4.0.tgz#27aec0cbed8ab4e2f0dad6feb4f0c9766ac3132f" integrity sha512-58b0K8pNUVZXGbua4IJQ+1K+E+jz3MkhDazZaaeKlD+sOLYR9iTHIbicV/I5K16ivYW6R9lONiT3dz8rMeFJ1w== -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jscad/array-utils@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@jscad/array-utils/-/array-utils-2.1.3.tgz#0de2eaaf122c43efc534a948c8d80a65483160e8" @@ -1569,26 +1533,6 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -1969,7 +1913,7 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0, acorn-walk@^8.1.1: +acorn-walk@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -1989,11 +1933,6 @@ acorn@^8.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== -acorn@^8.4.1: - version "8.8.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -2164,11 +2103,6 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2943,15 +2877,6 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g== -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" @@ -3059,11 +2984,6 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -3144,11 +3064,6 @@ corser@^2.0.1: resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -3403,11 +3318,6 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -4213,15 +4123,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" @@ -4242,13 +4143,6 @@ find-up@^2.1.0: dependencies: locate-path "^2.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -4783,18 +4677,6 @@ gulp-cli@^2.2.0: v8flags "^3.2.0" yargs "^7.1.0" -gulp-rollup-2@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/gulp-rollup-2/-/gulp-rollup-2-1.3.1.tgz#bbe1a8725ccaa4fc270627b298d1072d960fbe0c" - integrity sha512-sKkHsbhmcFBorQ3AHUME8Kifj3Ud7d6BwUMntCigolJQxzbXH8Ja688WRc3dolSlcp31cIWTtJf9r+090gC+Ew== - dependencies: - njfs "^1.2.5" - object-hash "^2.1.1" - rollup "^2.41.4" - through2 "^4.0.2" - vinyl "^2.2.1" - vinyl-sourcemaps-apply "^0.2.1" - gulp-typedoc@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/gulp-typedoc/-/gulp-typedoc-3.0.2.tgz#8466dd83d155658526c8180dab2fdd46e010770f" @@ -6339,14 +6221,6 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -6418,7 +6292,7 @@ magic-string@^0.25.2, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.4" -make-dir@^2.0.0, make-dir@^2.1.0: +make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -6433,7 +6307,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x, make-error@^1.1.1: +make-error@1.x: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -6770,11 +6644,6 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -njfs@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/njfs/-/njfs-1.2.5.tgz#082938e9fbf07044572e31c8624a81d9988b517a" - integrity sha512-Vnl1VDjSo7fO3RHAO5N9QrsyWaIpSTxHSwVoQOcepYAx3Lt+2JD/tUwgEJHfh1QqBjyy/H9WS5L79+hL+l99ug== - node-abi@^2.30.1, node-abi@^2.7.0: version "2.30.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" @@ -6990,11 +6859,6 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-hash@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" - integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== - object-inspect@^1.12.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -7184,7 +7048,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -7198,13 +7062,6 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -7457,18 +7314,6 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pirates@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -7810,7 +7655,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.1.1, readable-stream@^3.4.0: +"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -8253,13 +8098,6 @@ rollup@^2.41.2: optionalDependencies: fsevents "~2.3.1" -rollup@^2.41.4: - version "2.77.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.2.tgz#6b6075c55f9cc2040a5912e6e062151e42e2c4e3" - integrity sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g== - optionalDependencies: - fsevents "~2.3.2" - rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -8387,13 +8225,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -8551,14 +8382,6 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.16: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@^0.5.6, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" @@ -8572,7 +8395,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.6: +source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -9018,13 +8841,6 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" - integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== - dependencies: - readable-stream "3" - through@2, through@^2.3.8, through@~2.3, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -9132,25 +8948,6 @@ ts-jest@^26.5.4: semver "7.x" yargs-parser "20.x" -ts-node@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -9473,11 +9270,6 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -9564,14 +9356,7 @@ vinyl-sourcemap@^1.1.0: remove-bom-buffer "^3.0.0" vinyl "^2.0.0" -vinyl-sourcemaps-apply@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" - integrity sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw== - dependencies: - source-map "^0.5.1" - -vinyl@^2.0.0, vinyl@^2.2.1: +vinyl@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== @@ -9885,8 +9670,3 @@ yarnhook@^0.5.1: dependencies: execa "^4.0.3" find-parent-dir "^0.3.0" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 3dd6b1bc556ad1c52ee531fe8e71d140c75e5862 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 22:05:34 +0800 Subject: [PATCH 04/51] Added documentations --- gulpfile.esm.js/build/bundles.js | 8 +- gulpfile.esm.js/build/database.json | 60 ++++++++------ gulpfile.esm.js/build/docs/index.js | 75 +++++++++++------ gulpfile.esm.js/build/index.js | 6 +- .../build/{copyfile.js => misc.js} | 6 ++ gulpfile.esm.js/build/tabs.js | 9 ++- gulpfile.esm.js/build/utilities.js | 3 + package.json | 6 +- yarn.lock | 80 +------------------ 9 files changed, 111 insertions(+), 142 deletions(-) rename gulpfile.esm.js/build/{copyfile.js => misc.js} (77%) diff --git a/gulpfile.esm.js/build/bundles.js b/gulpfile.esm.js/build/bundles.js index 5f920215f..59b7d8362 100644 --- a/gulpfile.esm.js/build/bundles.js +++ b/gulpfile.esm.js/build/bundles.js @@ -2,9 +2,12 @@ import chalk from 'chalk'; import gulp from 'gulp'; import { rollup } from 'rollup'; import modules from '../../modules.json'; -import copy from './copyfile'; +import copy from './misc'; import { defaultConfig, getDb, isFolderModified } from './utilities'; +/** + * Transpile bundles to the build folder + */ export const buildBundles = (db) => { const isBundleModifed = (bundle) => { if (process.argv[3] === '--force') return true; @@ -40,8 +43,7 @@ export const buildBundles = (db) => { db.set(`bundle.${bundle}`, buildTime).write(); }; - const promises = moduleNames.map((bundle) => processBundle(bundle)); - return Promise.all(promises); + return Promise.all(moduleNames.map(processBundle)); }; export default gulp.series( diff --git a/gulpfile.esm.js/build/database.json b/gulpfile.esm.js/build/database.json index 82d62480b..f5552ef3a 100644 --- a/gulpfile.esm.js/build/database.json +++ b/gulpfile.esm.js/build/database.json @@ -1,30 +1,42 @@ { "tabs": { - "Game": 1659780674058, - "Repeat": 1659780674058, - "Sound": 1659780674058, - "StereoSound": 1659780674058, - "SoundMatrix": 1659780674058, - "CopyGc": 1659780674058, - "Pixnflix": 1659780674058, - "MarkSweep": 1659780674058, - "Csg": 1659780674058, - "Rune": 1659780674058, - "Curve": 1659780674058 + "Game": 1659788750315, + "Repeat": 1659788750315, + "Sound": 1659788750315, + "StereoSound": 1659788750315, + "SoundMatrix": 1659788750315, + "CopyGc": 1659788750315, + "Pixnflix": 1659788750315, + "MarkSweep": 1659788750315, + "Csg": 1659788750315, + "Rune": 1659788750315, + "Curve": 1659788750315 }, "bundle": { - "binary_tree": 1659781377620, - "scrabble": 1659781377620, - "pix_n_flix": 1659781377620, - "sound_matrix": 1659781377620, - "sound": 1659781377620, - "stereo_sound": 1659781377620, - "copy_gc": 1659781377620, - "mark_sweep": 1659781377620, - "repeat": 1659781377620, - "game": 1659781377620, - "rune": 1659781377620, - "curve": 1659781377620, - "csg": 1659781377620 + "binary_tree": 1659788929044, + "scrabble": 1659788929044, + "pix_n_flix": 1659788929044, + "sound_matrix": 1659788929044, + "sound": 1659788929044, + "stereo_sound": 1659788929044, + "copy_gc": 1659788929044, + "mark_sweep": 1659788929044, + "repeat": 1659788929044, + "game": 1659788929044, + "rune": 1659788929044, + "curve": 1659788929044, + "csg": 1659788929044 + }, + "docs": { + "repeat": 1659788939357, + "pix_n_flix": 1659788939357, + "binary_tree": 1659788939357, + "curve": 1659788939357, + "sound": 1659788939357, + "scrabble": 1659788939357, + "stereo_sound": 1659788939357, + "game": 1659788939357, + "rune": 1659788939357, + "csg": 1659788939357 } } \ No newline at end of file diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index e4d9e9209..c29bc4e23 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -5,6 +5,10 @@ import drawdown from './drawdown'; import { isFolderModified, getDb, cjsDirname } from '../utilities'; import modules from '../../../modules.json'; +/** + * Convert each element type (e.g. variable, function) to its respective HTML docstring + * to be displayed to users + */ const parsers = { Variable: (element, bundle) => { let desc; @@ -64,16 +68,39 @@ const parsers = { return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; }, }; + /** * Build the json documentation for the specified modules */ -export async function buildJsons() { +export const buildJsons = async (db) => { + const isBundleModifed = (bundle) => { + if (process.argv[3] === '--force') return true; + + const timestamp = db.get(`docs.${bundle}`).value() || 0; + return isFolderModified(`src/bundles/${bundle}`, timestamp); + }; + + const bundleNames = Object.keys(modules).filter(isBundleModifed); + if (bundleNames.length === 0) { + console.log('Documentation up to date'); + return; + } + + console.log( + chalk.greenBright('Building documentation for the following bundles:') + ); + console.log( + bundleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') + ); + const errHandler = (err) => { if (err) console.error(err); }; if (!fs.existsSync('build/jsons')) fs.mkdirSync(`build/jsons`, {}); + const buildTime = new Date().getTime(); + // Read from the TypeDoc output and retrieve the JSON relevant to the each module fs.readFile('build/docs.json', 'utf-8', (err, data) => { if (err) throw err; @@ -112,40 +139,32 @@ export async function buildJsons() { fs.writeFile( `build/jsons/${bundle}.json`, JSON.stringify(output, null, 2), - errHandler + (err) => { + if (err) console.error(err); + else { + db.set(`docs.${bundle}`, buildTime).write(); + } + } ); } fs.rm('build/jsons/output', { recursive: true, force: true }, errHandler); }); } -export const buildDocs = async (db) => { - const isBundleModifed = (bundle) => { - if (process.argv[3] === '--force') return true; - - const timestamp = db.get(`bundles.${bundle}`).value() || 0; - return isFolderModified(`src/bundles/${bundle}`, timestamp); - }; - - const bundleNames = Object.keys(modules).filter(isBundleModifed); - if (bundleNames.length === 0) { - console.log('Documentation up to date'); - return; - } - - console.log( - chalk.greenBright('Building documentation for the following bundles:') - ); - console.log( - bundleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') - ); - +/** + * Build the HTML documentation for all modules.\ + * \ + * TypeDoc always clears the directory after each build, so if you leave some modules out + * their documentation won't be properly included. Hence all modules have to be built at + * the same time. + */ +export const buildDocs = async () => { const app = new typedoc.Application(); app.options.addReader(new typedoc.TSConfigReader()); app.options.addReader(new typedoc.TypeDocReader()); app.bootstrap({ - entryPoints: bundleNames.map( + entryPoints: Object.keys(modules).map( (bundle) => `src/bundles/${bundle}/functions.ts` ), tsconfig: 'src/tsconfig.json', @@ -163,8 +182,12 @@ export const buildDocs = async (db) => { } }; +/** + * Build both JSONS and HTML documentation + */ export default async () => { + await buildDocs(); + const db = await getDb(); - await buildDocs(db); - await buildJsons(); + await buildJsons(db); }; diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js index 98dfea618..8b2fc684b 100644 --- a/gulpfile.esm.js/build/index.js +++ b/gulpfile.esm.js/build/index.js @@ -1,15 +1,15 @@ import { series } from 'gulp'; -import copy from './copyfile'; +import copy from './misc'; import { buildBundles } from './bundles'; import { buildTabs } from './tabs'; -import { buildDocs } from './docs'; +import { buildDocs, buildJsons } from './docs'; import { getDb } from './utilities'; export default series( Object.assign( async () => { const db = await getDb(); - await Promise.all([buildBundles(db), buildTabs(db), buildDocs(db)]); + await Promise.all([buildBundles(db), buildTabs(db), buildDocs(), buildJsons(db)]); }, { displayName: 'build', diff --git a/gulpfile.esm.js/build/copyfile.js b/gulpfile.esm.js/build/misc.js similarity index 77% rename from gulpfile.esm.js/build/copyfile.js rename to gulpfile.esm.js/build/misc.js index dea637b31..6b1cb3f18 100644 --- a/gulpfile.esm.js/build/copyfile.js +++ b/gulpfile.esm.js/build/misc.js @@ -1,9 +1,15 @@ import gulp, { series } from 'gulp'; import { getDbPath } from './utilities'; +/** + * Copy `modules.json` to the build folder + */ export const copyModules = () => gulp.src('modules.json').pipe(gulp.dest('build/')); +/** + * Copy `database.json` to the build folder + */ export const copyDatabase = () => gulp.src(getDbPath()).pipe(gulp.dest('build/')); diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js index a7df26d90..c7411832e 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/gulpfile.esm.js/build/tabs.js @@ -8,9 +8,12 @@ import { removeDuplicates, defaultConfig, } from './utilities'; -import copy from './copyfile'; +import copy from './misc'; import modules from '../../modules.json'; +/** + * Transpile tabs to the build folder + */ export const buildTabs = (db) => { const isTabModifed = (tabName) => { if (process.argv[3] === '--force') return true; @@ -55,9 +58,7 @@ export const buildTabs = (db) => { db.set(`tabs.${tabName}`, buildTime).write(); }; - const promises = tabNames.map(processTab); - - return Promise.all(promises); + return Promise.all(tabNames.map(processTab)); }; export default gulp.series( diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index f687dfe9a..14f623499 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -18,6 +18,9 @@ import { SUPPRESSED_WARNINGS, } from './constants'; +/** + * Default configuration used by rollup for transpiling both tabs and bundles + */ export const defaultConfig = { onwarn(warning, warn) { if (SUPPRESSED_WARNINGS.includes(warning.code)) return; diff --git a/package.json b/package.json index c63207dac..3c2919a85 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "lint:fix": "Lint code and fix automatically fixable problems", "build": "Lint code, then build modules and documentation", "build:bundles": "Transpile modules into the `build/` folder and copy over `modules.json`", + "build:tabs": "Transpile tabs into the `build/` folder and copy over `modules.json`", "build:docs": "Generate TypeDocs in the `build/documentation/` folder", - "rebuild": "Build only modules whose files have been modified since the last build/rebuild", "serve": "Start the HTTP server to serve all files in `build/`, with the same directory structure", "dev": "Rebuild modules, then serve", "prepare": "Enable git hooks", @@ -27,9 +27,8 @@ "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildBundles", "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildTabs", "build:docs": "gulp buildDocs", - "rebuild": "./node_modules/.bin/tsc --project src/tsconfig.json && rollup -c scripts/build/rollup.config.js --quick", "serve": "http-server --cors=* -c-1 -p 8022 ./build", - "dev": "yarn rebuild && yarn serve", + "dev": "yarn build && yarn serve", "prepare": "husky install", "test": "jest --verbose", "test:watch": "jest --watch" @@ -68,7 +67,6 @@ "esm": "^3.2.25", "generate-template-files": "^3.0.0", "gulp": "^4.0.2", - "gulp-typedoc": "^3.0.2", "http-server": "^0.12.3", "husky": "5", "jest": "^26.6.3", diff --git a/yarn.lock b/yarn.lock index c681c6d84..0b0d26b48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1986,11 +1986,6 @@ ansi-colors@^3.2.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -3363,7 +3358,7 @@ duplexer@0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= -duplexer@^0.1.1, duplexer@^0.1.2, duplexer@~0.1.1: +duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -3873,19 +3868,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-stream@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65" - integrity sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA== - dependencies: - duplexer "^0.1.1" - from "^0.1.7" - map-stream "0.0.7" - pause-stream "^0.0.11" - split "^1.0.1" - stream-combiner "^0.2.2" - through "^2.3.8" - eventemitter3@^4.0.0, eventemitter3@^4.0.7: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -4017,7 +3999,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fancy-log@^1.3.2, fancy-log@^1.3.3: +fancy-log@^1.3.2: version "1.3.3" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== @@ -4246,11 +4228,6 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -from@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -4677,17 +4654,6 @@ gulp-cli@^2.2.0: v8flags "^3.2.0" yargs "^7.1.0" -gulp-typedoc@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gulp-typedoc/-/gulp-typedoc-3.0.2.tgz#8466dd83d155658526c8180dab2fdd46e010770f" - integrity sha512-f7TVA6/fmZMU1vAskeZbeqBRpHbkFiY49r8xQV8aFIJSoo/Vvw67Tsp9jfUlPAQPC/LYBn/dMg3WmROSbdzskw== - dependencies: - ansi-colors "^4.1.1" - event-stream "^4.0.1" - fancy-log "^1.3.3" - plugin-error "^1.0.1" - semver "^7.3.5" - gulp@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa" @@ -6352,11 +6318,6 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" - integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ== - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -7250,13 +7211,6 @@ path@^0.12.7: process "^0.11.1" util "^0.10.3" -pause-stream@^0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== - dependencies: - through "~2.3" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -7321,16 +7275,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -plugin-error@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" - integrity sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA== - dependencies: - ansi-colors "^1.0.1" - arr-diff "^4.0.0" - arr-union "^3.1.0" - extend-shallow "^3.0.2" - popper.js@^1.14.4, popper.js@^1.16.1: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -8453,13 +8397,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -8519,14 +8456,6 @@ steno@^0.4.1: dependencies: graceful-fs "^4.1.3" -stream-combiner@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" - integrity sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ== - dependencies: - duplexer "~0.1.1" - through "~2.3.4" - stream-exhaust@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" @@ -8841,11 +8770,6 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, through@^2.3.8, through@~2.3, through@~2.3.4: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - time-stamp@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" From 25768e051f3695c3910448e88dc4f06584b67af3 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 22:07:42 +0800 Subject: [PATCH 05/51] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cbd7e30b5..69972dd8b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ build/* /package-lock.json coverage/ -scripts/rollup/database.json +gulpfile.esm.js/build/database.json # Compiled source # ################### From 887aba91e9475431bac68a74a40324d7bcea7627 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 22:09:52 +0800 Subject: [PATCH 06/51] Delete database.json --- gulpfile.esm.js/build/database.json | 42 ----------------------------- 1 file changed, 42 deletions(-) delete mode 100644 gulpfile.esm.js/build/database.json diff --git a/gulpfile.esm.js/build/database.json b/gulpfile.esm.js/build/database.json deleted file mode 100644 index f5552ef3a..000000000 --- a/gulpfile.esm.js/build/database.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "tabs": { - "Game": 1659788750315, - "Repeat": 1659788750315, - "Sound": 1659788750315, - "StereoSound": 1659788750315, - "SoundMatrix": 1659788750315, - "CopyGc": 1659788750315, - "Pixnflix": 1659788750315, - "MarkSweep": 1659788750315, - "Csg": 1659788750315, - "Rune": 1659788750315, - "Curve": 1659788750315 - }, - "bundle": { - "binary_tree": 1659788929044, - "scrabble": 1659788929044, - "pix_n_flix": 1659788929044, - "sound_matrix": 1659788929044, - "sound": 1659788929044, - "stereo_sound": 1659788929044, - "copy_gc": 1659788929044, - "mark_sweep": 1659788929044, - "repeat": 1659788929044, - "game": 1659788929044, - "rune": 1659788929044, - "curve": 1659788929044, - "csg": 1659788929044 - }, - "docs": { - "repeat": 1659788939357, - "pix_n_flix": 1659788939357, - "binary_tree": 1659788939357, - "curve": 1659788939357, - "sound": 1659788939357, - "scrabble": 1659788939357, - "stereo_sound": 1659788939357, - "game": 1659788939357, - "rune": 1659788939357, - "csg": 1659788939357 - } -} \ No newline at end of file From 7699474c8e32835731b0000556d6b64fcd9a31d8 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 6 Aug 2022 22:14:02 +0800 Subject: [PATCH 07/51] Update package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 3c2919a85..65f6863bb 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,7 @@ "jest": { "roots": [ "/src/bundles", - "/src/tabs", - "/scripts" + "/src/tabs" ] } } From 0ffce2694f39d912fef9aa682cbf0d2ae7b538b9 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sun, 7 Aug 2022 01:13:06 +0800 Subject: [PATCH 08/51] Update index.js Fixed README.md not being copied --- gulpfile.esm.js/build/docs/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index c29bc4e23..b04d800eb 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -169,7 +169,6 @@ export const buildDocs = async () => { ), tsconfig: 'src/tsconfig.json', theme: 'typedoc-modules-theme', - readme: `${cjsDirname()}/README.md`, excludeInternal: true, categorizeByGroup: true, name: 'Source Academy Modules', @@ -177,8 +176,12 @@ export const buildDocs = async () => { const project = app.convert(); if (project) { - await app.generateDocs(project, 'build/documentation'); - await app.generateJson(project, 'build/docs.json'); + const docsTask = app.generateDocs(project, 'build/documentation'); + const jsonTask = app.generateJson(project, 'build/docs.json'); + await Promise.all([docsTask, jsonTask]); + + // For some reason typedoc's not working, so do a manual copy + fs.copyFileSync(`${cjsDirname()}/docs/README.md`, 'build/documentation/README.md') } }; From 83d5e6d83d1a969847d096510e57775d93835125 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sun, 7 Aug 2022 02:52:31 +0800 Subject: [PATCH 09/51] Build scripts run if build folders are empty or missing --- gulpfile.esm.js/build/bundles.js | 18 ++++++++++----- gulpfile.esm.js/build/docs/index.js | 34 +++++++++++++++++++---------- gulpfile.esm.js/build/index.js | 7 +++++- gulpfile.esm.js/build/tabs.js | 13 +++++++---- gulpfile.esm.js/build/utilities.js | 12 ++++++++++ 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/gulpfile.esm.js/build/bundles.js b/gulpfile.esm.js/build/bundles.js index 59b7d8362..a3b4f2139 100644 --- a/gulpfile.esm.js/build/bundles.js +++ b/gulpfile.esm.js/build/bundles.js @@ -3,7 +3,12 @@ import gulp from 'gulp'; import { rollup } from 'rollup'; import modules from '../../modules.json'; import copy from './misc'; -import { defaultConfig, getDb, isFolderModified } from './utilities'; +import { + defaultConfig, + getDb, + isFolderModified, + shouldBuildAll, +} from './utilities'; /** * Transpile bundles to the build folder @@ -15,16 +20,19 @@ export const buildBundles = (db) => { return isFolderModified(`src/bundles/${bundle}`, timestamp); }; - const moduleNames = Object.keys(modules).filter(isBundleModifed); + const bundleNames = Object.keys(modules); + const filteredBundles = shouldBuildAll('bundles') + ? bundleNames + : bundleNames.filter(isBundleModifed); - if (moduleNames.length === 0) { + if (filteredBundles.length === 0) { console.log('All bundles up to date'); return null; } console.log(chalk.greenBright('Building bundles for the following modules:')); console.log( - moduleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') + filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') ); const buildTime = new Date().getTime(); @@ -43,7 +51,7 @@ export const buildBundles = (db) => { db.set(`bundle.${bundle}`, buildTime).write(); }; - return Promise.all(moduleNames.map(processBundle)); + return Promise.all(filteredBundles.map(processBundle)); }; export default gulp.series( diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index b04d800eb..b1c91c8d2 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -2,7 +2,12 @@ import fs from 'fs'; import * as typedoc from 'typedoc'; import chalk from 'chalk'; import drawdown from './drawdown'; -import { isFolderModified, getDb, cjsDirname } from '../utilities'; +import { + isFolderModified, + getDb, + cjsDirname, + shouldBuildAll, +} from '../utilities'; import modules from '../../../modules.json'; /** @@ -74,14 +79,16 @@ const parsers = { */ export const buildJsons = async (db) => { const isBundleModifed = (bundle) => { - if (process.argv[3] === '--force') return true; - const timestamp = db.get(`docs.${bundle}`).value() || 0; return isFolderModified(`src/bundles/${bundle}`, timestamp); }; - const bundleNames = Object.keys(modules).filter(isBundleModifed); - if (bundleNames.length === 0) { + const bundleNames = Object.keys(modules); + const filteredBundles = shouldBuildAll('jsons') + ? bundleNames + : bundleNames.filter(isBundleModifed); + + if (filteredBundles.length === 0) { console.log('Documentation up to date'); return; } @@ -90,7 +97,7 @@ export const buildJsons = async (db) => { chalk.greenBright('Building documentation for the following bundles:') ); console.log( - bundleNames.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') + filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') ); const errHandler = (err) => { @@ -139,8 +146,8 @@ export const buildJsons = async (db) => { fs.writeFile( `build/jsons/${bundle}.json`, JSON.stringify(output, null, 2), - (err) => { - if (err) console.error(err); + (error) => { + if (error) console.error(error); else { db.set(`docs.${bundle}`, buildTime).write(); } @@ -149,7 +156,7 @@ export const buildJsons = async (db) => { } fs.rm('build/jsons/output', { recursive: true, force: true }, errHandler); }); -} +}; /** * Build the HTML documentation for all modules.\ @@ -176,12 +183,15 @@ export const buildDocs = async () => { const project = app.convert(); if (project) { - const docsTask = app.generateDocs(project, 'build/documentation'); - const jsonTask = app.generateJson(project, 'build/docs.json'); + const docsTask = app.generateDocs(project, 'build/documentation'); + const jsonTask = app.generateJson(project, 'build/docs.json'); await Promise.all([docsTask, jsonTask]); // For some reason typedoc's not working, so do a manual copy - fs.copyFileSync(`${cjsDirname()}/docs/README.md`, 'build/documentation/README.md') + fs.copyFileSync( + `${cjsDirname()}/docs/README.md`, + 'build/documentation/README.md' + ); } }; diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js index 8b2fc684b..d34bd19d7 100644 --- a/gulpfile.esm.js/build/index.js +++ b/gulpfile.esm.js/build/index.js @@ -9,7 +9,12 @@ export default series( Object.assign( async () => { const db = await getDb(); - await Promise.all([buildBundles(db), buildTabs(db), buildDocs(), buildJsons(db)]); + await Promise.all([ + buildBundles(db), + buildTabs(db), + buildDocs(), + buildJsons(db), + ]); }, { displayName: 'build', diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js index c7411832e..7ba9987fc 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/gulpfile.esm.js/build/tabs.js @@ -7,6 +7,7 @@ import { getDb, removeDuplicates, defaultConfig, + shouldBuildAll, } from './utilities'; import copy from './misc'; import modules from '../../modules.json'; @@ -23,14 +24,18 @@ export const buildTabs = (db) => { const tabNames = removeDuplicates( Object.values(modules).flatMap((x) => x.tabs) - ).filter(isTabModifed); + ); - if (tabNames.length === 0) { + const filteredTabs = shouldBuildAll('tabs') + ? tabNames + : tabNames.filter(isTabModifed); + + if (filteredTabs.length === 0) { return null; } console.log(chalk.greenBright('Building the following tabs:')); - console.log(tabNames.map((x) => `• ${chalk.blue(x)}`).join('\n')); + console.log(filteredTabs.map((x) => `• ${chalk.blue(x)}`).join('\n')); const buildTime = new Date().getTime(); @@ -58,7 +63,7 @@ export const buildTabs = (db) => { db.set(`tabs.${tabName}`, buildTime).write(); }; - return Promise.all(tabNames.map(processTab)); + return Promise.all(filteredTabs.map(processTab)); }; export default gulp.series( diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index 14f623499..beaa14e56 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -118,3 +118,15 @@ export function getDb() { export function removeDuplicates(arr) { return [...new Set(arr)]; } + +/** + * Checks if the given output directory is empty, to determine + * if the given build script should execute regardless of the last build time + */ +export const shouldBuildAll = (outputDir) => { + if (process.argv[3] === '--force') return true; + + if (!fs.existsSync(`build/${outputDir}`)) return true; + + return fs.readdirSync(`build/${outputDir}`).length === 0; +}; From b8fe8b94ec254c2b0d5b91fac0d511c248e7a14a Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 9 Aug 2022 12:00:30 +0800 Subject: [PATCH 10/51] Combined eslintrcs --- src/.eslintrc.js => .eslintrc.js | 25 ++++++++++- gulpfile.esm.js/.eslintrc.js | 71 -------------------------------- 2 files changed, 23 insertions(+), 73 deletions(-) rename src/.eslintrc.js => .eslintrc.js (84%) delete mode 100644 gulpfile.esm.js/.eslintrc.js diff --git a/src/.eslintrc.js b/.eslintrc.js similarity index 84% rename from src/.eslintrc.js rename to .eslintrc.js index 3036c21df..ce666fcfc 100644 --- a/src/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - extends: ['../.eslintrc.base.js', 'plugin:prettier/recommended'], + extends: ['.eslintrc.base.js', 'plugin:prettier/recommended'], root: true, parserOptions: { @@ -59,7 +59,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.json', + project: './src/tsconfig.json', tsconfigRootDir: __dirname, }, @@ -140,5 +140,26 @@ module.exports = { ], }, }, + { + files: ['gulpfile.esm.js/**js'], + extends: ['airbnb', 'plugin:prettier/recommended'], + parserOptions: { + ecmaVersion: '2020', + }, + rules: { + 'import/no-extraneous-dependencies': 0, + 'no-console': 0, + 'no-continue': 0, + 'no-param-reassign': 0, + 'no-restricted-syntax': 0, + 'prefer-const': 0, + 'prettier/prettier': 1, // Was 2 + }, + }, + { + files: ['./**/__tests__/**.test.js', './**/__tests__/**.test.ts'], + extends: ['airbnb', 'plugin:prettier/recommended', 'plugin:jest/recommeended'], + plugins: ['jest'] + } ], }; diff --git a/gulpfile.esm.js/.eslintrc.js b/gulpfile.esm.js/.eslintrc.js deleted file mode 100644 index 0c13ba514..000000000 --- a/gulpfile.esm.js/.eslintrc.js +++ /dev/null @@ -1,71 +0,0 @@ -module.exports = { - extends: ['../.eslintrc.base.js', 'plugin:prettier/recommended'], - - root: true, - parserOptions: { - sourceType: 'module', - }, - - rules: { - 'func-style': 0, - indent: [ - 1, - 2, // Was "tabs" - { - SwitchCase: 1, // Same - // VariableDeclarator: 1, - // outerIIFEBody: 1, - // MemberExpression: 1, - // FunctionDeclaration: { - // parameters: 1, - // body: 1 - // }, - // FunctionExpression: { - // parameters: 1, - // body: 1 - // }, - // StaticBlock: { - // body: 1 - // }, - // CallExpression: { - // arguments: 1, - // }, - // ArrayExpression: 1, - // ObjectExpression: 1, - // ImportDeclaration: 1, - // flatTernaryExpressions: false, - // offsetTernaryExpressions: false, - // ignoreComments: false - }, - ], - quotes: [ - 1, - 'single', // Was "double" - { - avoidEscape: true, // Same - // allowTemplateLiterals: false - }, - ], - - 'prettier/prettier': 1, // Was 2 - }, - - overrides: [ - { - files: ['**/**.js'], - extends: ['airbnb', 'plugin:prettier/recommended'], - parserOptions: { - ecmaVersion: '2020', - }, - rules: { - 'import/no-extraneous-dependencies': 0, - 'no-console': 0, - 'no-continue': 0, - 'no-param-reassign': 0, - 'no-restricted-syntax': 0, - 'prefer-const': 0, - 'prettier/prettier': 1, // Was 2 - }, - }, - ], -}; From 9f7fe0297692cf1a21e30118c7f6045d0f6ff0fe Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 9 Aug 2022 12:00:47 +0800 Subject: [PATCH 11/51] Adding tests to the build scripts --- gulpfile.esm.js/_tests__/tabs.test.js | 17 +++++++++++++++++ gulpfile.esm.js/build/tabs.js | 8 ++++++-- package.json | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 gulpfile.esm.js/_tests__/tabs.test.js diff --git a/gulpfile.esm.js/_tests__/tabs.test.js b/gulpfile.esm.js/_tests__/tabs.test.js new file mode 100644 index 000000000..2e3a2358d --- /dev/null +++ b/gulpfile.esm.js/_tests__/tabs.test.js @@ -0,0 +1,17 @@ +import { convertRawTab } from "../build/tabs"; + +describe('Testing raw tab processing', () => { + test('Converts React tab properly', () => { + const rawTab = '(function (React) {})(React)'; + const result = convertRawTab(rawTab); + + expect(result).toEqual('(function (React) {})'); + }) + + test('Converts ReactDOM tab properly', () => { + const rawTab = '(function (React, ReactDOM) {})(React, ReactDOM)'; + const result = convertRawTab(rawTab); + + expect(result).toEqual('(function (React, ReactDOM) {})'); + }) +}) \ No newline at end of file diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js index 7ba9987fc..5425914b4 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/gulpfile.esm.js/build/tabs.js @@ -12,6 +12,11 @@ import { import copy from './misc'; import modules from '../../modules.json'; +export const convertRawTab = (rawTab) => { + const lastBracket = rawTab.lastIndexOf('('); + return `${rawTab.substring(0, lastBracket)})` +} + /** * Transpile tabs to the build folder */ @@ -57,8 +62,7 @@ export const buildTabs = (db) => { }); const rawTab = fs.readFileSync(tabFile, 'utf-8'); - const lastBracket = rawTab.lastIndexOf('('); - fs.writeFileSync(tabFile, `${rawTab.substring(0, lastBracket)})`); + fs.writeFileSync(tabFile, convertRawTab(rawTab)); db.set(`tabs.${tabName}`, buildTime).write(); }; diff --git a/package.json b/package.json index 65f6863bb..4d86b4df7 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,8 @@ "jest": { "roots": [ "/src/bundles", - "/src/tabs" + "/src/tabs", + "/gulpfile.esm.js" ] } } From df8daf5a740ca425ee97d4ca67bf70a6a77482b3 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 9 Aug 2022 12:09:10 +0800 Subject: [PATCH 12/51] Adding eslint jest plugin --- package.json | 1 + yarn.lock | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d86b4df7..5e67b4604 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-import-resolver-typescript": "^2.7.1", "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jest": "^26.8.1", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.4", diff --git a/yarn.lock b/yarn.lock index 0b0d26b48..b0cab95f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1831,6 +1831,14 @@ "@typescript-eslint/types" "5.18.0" "@typescript-eslint/visitor-keys" "5.18.0" +"@typescript-eslint/scope-manager@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz#509d7fa540a2c58f66bdcfcf278a3fa79002e18d" + integrity sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + "@typescript-eslint/type-utils@5.18.0": version "5.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74" @@ -1845,6 +1853,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e" integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw== +"@typescript-eslint/types@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.33.0.tgz#d41c584831805554b063791338b0220b613a275b" + integrity sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw== + "@typescript-eslint/typescript-estree@5.18.0": version "5.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474" @@ -1858,6 +1871,19 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz#02d9c9ade6f4897c09e3508c27de53ad6bfa54cf" + integrity sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.18.0": version "5.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855" @@ -1870,6 +1896,18 @@ eslint-scope "^5.1.1" eslint-utils "^3.0.0" +"@typescript-eslint/utils@^5.10.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.33.0.tgz#46797461ce3146e21c095d79518cc0f8ec574038" + integrity sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/typescript-estree" "5.33.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + "@typescript-eslint/visitor-keys@5.18.0": version "5.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60" @@ -1878,6 +1916,14 @@ "@typescript-eslint/types" "5.18.0" eslint-visitor-keys "^3.0.0" +"@typescript-eslint/visitor-keys@5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz#fbcbb074e460c11046e067bc3384b5d66b555484" + integrity sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw== + dependencies: + "@typescript-eslint/types" "5.33.0" + eslint-visitor-keys "^3.3.0" + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3676,6 +3722,13 @@ eslint-plugin-import@^2.26.0: resolve "^1.22.0" tsconfig-paths "^3.14.1" +eslint-plugin-jest@^26.8.1: + version "26.8.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-26.8.1.tgz#f870a9912f6fe65e16e181cbb368229cd5889c65" + integrity sha512-0+OSTIPjIIfl9heUxZyuU1gDEMW2sLvoVBGwiCIp+iEqUYt0Yqr5M5BjAyVShJaisICmCALdVv0nd1UDX/QaYw== + dependencies: + "@typescript-eslint/utils" "^5.10.0" + eslint-plugin-jsx-a11y@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz#cdbf2df901040ca140b6ec14715c988889c2a6d8" @@ -4556,7 +4609,7 @@ globby@10.0.1: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.4: +globby@^11.0.4, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -8154,6 +8207,13 @@ semver@^7.3.5: dependencies: lru-cache "^7.4.0" +semver@^7.3.7: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" From 148b15ed992f5298b92d57394c7ced2e8c485569 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Tue, 9 Aug 2022 12:57:20 +0800 Subject: [PATCH 13/51] Updated babel and jest to work with js files --- .eslintrc.js | 2 +- babel.config.js | 9 ++++++- package.json | 1 + yarn.lock | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ce666fcfc..48d06accb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -141,7 +141,7 @@ module.exports = { }, }, { - files: ['gulpfile.esm.js/**js'], + files: ['gulpfile.esm.js/**.js'], extends: ['airbnb', 'plugin:prettier/recommended'], parserOptions: { ecmaVersion: '2020', diff --git a/babel.config.js b/babel.config.js index 38660c587..fac36d9b9 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,4 @@ -module.exports = { +const defaultConfig = { /** * - **script** - Parse the file using the ECMAScript Script grammar. No import/export statements allowed, and files are not in strict mode. * - **module** - Parse the file using the ECMAScript Module grammar. Files are automatically strict, and import/export statements are allowed. @@ -89,3 +89,10 @@ module.exports = { ], ], }; + +module.exports = api => api.env('test') ? { + ...defaultConfig, + plugins: [ + ["babel-plugin-transform-import-meta", { module: "ES6" }] + ], +} : defaultConfig; diff --git a/package.json b/package.json index 5e67b4604..220a3a2c8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/parser": "^5.18.0", "babel-jest": "^26.6.3", + "babel-plugin-transform-import-meta": "^2.2.0", "chalk": "^4.1.2", "cross-env": "^7.0.3", "eslint": "^8.12.0", diff --git a/yarn.lock b/yarn.lock index b0cab95f8..7a07c2953 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,13 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.8": version "7.13.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" @@ -292,11 +299,21 @@ dependencies: "@babel/types" "^7.12.13" +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -330,6 +347,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.13.13": version "7.13.13" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df" @@ -340,6 +366,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409" integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ== +"@babel/parser@^7.18.10": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" @@ -997,6 +1028,15 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" +"@babel/template@^7.4.4": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.13": version "7.13.13" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.13.tgz#39aa9c21aab69f74d948a486dd28a2dbdbf5114d" @@ -1044,6 +1084,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" + integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2461,6 +2510,14 @@ babel-plugin-polyfill-regenerator@^0.1.2: dependencies: "@babel/helper-define-polyfill-provider" "^0.1.5" +babel-plugin-transform-import-meta@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.0.tgz#a8a6a9a1bcb8e75ac5a44c6848bf1deb4ebfeb78" + integrity sha512-+DNF2SJAj2Pd0b1sObz+hyzNgUlI9DccPtMcF7ulMM0BxPrMF83ERjvPQwcQ9FRFSddWcC7DOw0FuyWgkQRoqg== + dependencies: + "@babel/template" "^7.4.4" + tslib "^2.4.0" + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -8952,6 +9009,11 @@ tslib@^2.3.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@~1.13.0: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" From 5ebcbd0ff269772a9d51300d6c849fc40d82eb21 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Wed, 10 Aug 2022 14:16:18 +0800 Subject: [PATCH 14/51] Updated tests --- gulpfile.esm.js/{_tests__ => __tests__}/tabs.test.js | 4 ++-- gulpfile.esm.js/build/docs/index.js | 2 +- gulpfile.esm.js/build/index.js | 3 +-- gulpfile.esm.js/build/utilities.js | 2 +- src/typings/type_helpers.ts | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) rename gulpfile.esm.js/{_tests__ => __tests__}/tabs.test.js (76%) diff --git a/gulpfile.esm.js/_tests__/tabs.test.js b/gulpfile.esm.js/__tests__/tabs.test.js similarity index 76% rename from gulpfile.esm.js/_tests__/tabs.test.js rename to gulpfile.esm.js/__tests__/tabs.test.js index 2e3a2358d..f663d5349 100644 --- a/gulpfile.esm.js/_tests__/tabs.test.js +++ b/gulpfile.esm.js/__tests__/tabs.test.js @@ -2,14 +2,14 @@ import { convertRawTab } from "../build/tabs"; describe('Testing raw tab processing', () => { test('Converts React tab properly', () => { - const rawTab = '(function (React) {})(React)'; + const rawTab = '(function (React) {}(React))'; const result = convertRawTab(rawTab); expect(result).toEqual('(function (React) {})'); }) test('Converts ReactDOM tab properly', () => { - const rawTab = '(function (React, ReactDOM) {})(React, ReactDOM)'; + const rawTab = '(function (React, ReactDOM) {}(React, ReactDOM))'; const result = convertRawTab(rawTab); expect(result).toEqual('(function (React, ReactDOM) {})'); diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index b1c91c8d2..dc4c02b47 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -123,7 +123,6 @@ export const buildJsons = async (db) => { for (const bundle of bundles) { const moduleDocs = parsedJSON.find((x) => x.name === bundle); - const output = {}; if (!moduleDocs || !moduleDocs.children) { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}` @@ -131,6 +130,7 @@ export const buildJsons = async (db) => { continue; } + const output = {}; moduleDocs.children.forEach((element) => { if (parsers[element.kindString]) { output[element.name] = parsers[element.kindString](element, bundle); diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js index d34bd19d7..dfcc9aee0 100644 --- a/gulpfile.esm.js/build/index.js +++ b/gulpfile.esm.js/build/index.js @@ -12,8 +12,7 @@ export default series( await Promise.all([ buildBundles(db), buildTabs(db), - buildDocs(), - buildJsons(db), + buildDocs().then(() => buildJsons(db)), ]); }, { diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index beaa14e56..19dda62ca 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -105,7 +105,7 @@ export function cjsDirname() { * Get the path to the database file */ export function getDbPath() { - return join(dirname(fileURLToPath(import.meta.url)), `${DATABASE_NAME}.json`); + return join(cjsDirname(), `${DATABASE_NAME}.json`); } /** diff --git a/src/typings/type_helpers.ts b/src/typings/type_helpers.ts index 979da8be7..355fdb6d5 100644 --- a/src/typings/type_helpers.ts +++ b/src/typings/type_helpers.ts @@ -1,4 +1,4 @@ -import { Context } from 'js-slang'; +import type { Context } from 'js-slang'; /** * DebuggerContext type used by frontend From 2b47b1e439581550574dbd4c972314a21e520bae Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 11 Aug 2022 02:46:00 +0800 Subject: [PATCH 15/51] Changed to using the fsPromises lib --- gulpfile.esm.js/build/docs/index.js | 78 +++++++++++++------------- gulpfile.esm.js/build/tabs.js | 10 ++-- gulpfile.esm.js/build/utilities.js | 11 +--- gulpfile.esm.js/index.js | 11 ++-- gulpfile.esm.js/templates/index.js | 30 ++++++++++ gulpfile.esm.js/templates/utilities.js | 10 ++++ gulpfile.esm.js/utilities.js | 10 ++++ package.json | 4 +- 8 files changed, 103 insertions(+), 61 deletions(-) create mode 100644 gulpfile.esm.js/templates/index.js create mode 100644 gulpfile.esm.js/templates/utilities.js create mode 100644 gulpfile.esm.js/utilities.js diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index dc4c02b47..9d8d1cb5a 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -1,4 +1,4 @@ -import fs from 'fs'; +import { constants as fsConstants, promises as fs } from 'fs'; import * as typedoc from 'typedoc'; import chalk from 'chalk'; import drawdown from './drawdown'; @@ -15,7 +15,7 @@ import modules from '../../../modules.json'; * to be displayed to users */ const parsers = { - Variable: (element, bundle) => { + Variable(element, bundle) { let desc; if (element.comment && element.comment.shortText) { desc = drawdown(element.comment.shortText); @@ -33,7 +33,7 @@ const parsers = { return `

    ${element.name}${typeStr}

    ${desc}
    `; }, - Function: (element, bundle) => { + Function(element, bundle) { if (!element.signatures || element.signatures[0] === undefined) throw new Error( `Error: ${bundle}: Unable to find a signature for function ${element.name}!` @@ -44,7 +44,7 @@ const parsers = { // Form the parameter string for the function let paramStr; - if (!signature.parameters) paramStr = `()`; + if (!signature.parameters) paramStr = '()'; else paramStr = `(${signature.parameters .map((param) => { @@ -55,7 +55,7 @@ const parsers = { // Form the result representation for the function let resultStr; - if (!signature.type) resultStr = `void`; + if (!signature.type) resultStr = 'void'; else resultStr = signature.type.name; let desc; @@ -100,38 +100,40 @@ export const buildJsons = async (db) => { filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') ); - const errHandler = (err) => { - if (err) console.error(err); - }; - - if (!fs.existsSync('build/jsons')) fs.mkdirSync(`build/jsons`, {}); + try { + await fs.access('build/jsons', fsConstants.F_OK); + } catch (_error) { + await fs.mkdir('build/jsons'); + } const buildTime = new Date().getTime(); - // Read from the TypeDoc output and retrieve the JSON relevant to the each module - fs.readFile('build/docs.json', 'utf-8', (err, data) => { - if (err) throw err; + const docsFile = await fs.readFile('build/docs.json'); + const parsedJSON = JSON.parse(docsFile).children; - const parsedJSON = JSON.parse(data).children; + if (!parsedJSON) { + throw new Error('Failed to parse docs.json'); + } - if (!parsedJSON) { - throw new Error('Failed to parse docs.json'); - } + const bundles = Object.keys(modules).map((bundle) => { + const moduleDocs = parsedJSON.find((x) => x.name === bundle); - const bundles = Object.keys(modules); + if (!moduleDocs || !moduleDocs.children) { + console.warn( + `${chalk.yellow('Warning:')} No documentation found for ${bundle}` + ); + return undefined; + } - for (const bundle of bundles) { - const moduleDocs = parsedJSON.find((x) => x.name === bundle); + return [bundle, moduleDocs.children]; + }); - if (!moduleDocs || !moduleDocs.children) { - console.warn( - `${chalk.yellow('Warning:')} No documentation found for ${bundle}` - ); - continue; - } + await Promise.all( + bundles.map(async ([bundle, docs]) => { + if (!docs) return; const output = {}; - moduleDocs.children.forEach((element) => { + docs.forEach((element) => { if (parsers[element.kindString]) { output[element.name] = parsers[element.kindString](element, bundle); } else { @@ -143,19 +145,17 @@ export const buildJsons = async (db) => { } }); - fs.writeFile( + await fs.writeFile( `build/jsons/${bundle}.json`, - JSON.stringify(output, null, 2), - (error) => { - if (error) console.error(error); - else { - db.set(`docs.${bundle}`, buildTime).write(); - } - } + JSON.stringify(output, null, 2) ); - } - fs.rm('build/jsons/output', { recursive: true, force: true }, errHandler); - }); + + db.set(`docs.${bundle}`, buildTime); + }) + ); + + // Read from the TypeDoc output and retrieve the JSON relevant to the each module + await fs.rm('build/jsons/output', { recursive: true, force: true }); }; /** @@ -188,7 +188,7 @@ export const buildDocs = async () => { await Promise.all([docsTask, jsonTask]); // For some reason typedoc's not working, so do a manual copy - fs.copyFileSync( + await fs.copyFile( `${cjsDirname()}/docs/README.md`, 'build/documentation/README.md' ); diff --git a/gulpfile.esm.js/build/tabs.js b/gulpfile.esm.js/build/tabs.js index 5425914b4..6b66e06c1 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/gulpfile.esm.js/build/tabs.js @@ -1,7 +1,7 @@ import gulp from 'gulp'; import { rollup } from 'rollup'; import chalk from 'chalk'; -import fs from 'fs'; +import { promises as fs } from 'fs'; import { isFolderModified, getDb, @@ -14,8 +14,8 @@ import modules from '../../modules.json'; export const convertRawTab = (rawTab) => { const lastBracket = rawTab.lastIndexOf('('); - return `${rawTab.substring(0, lastBracket)})` -} + return `${rawTab.substring(0, lastBracket)})`; +}; /** * Transpile tabs to the build folder @@ -61,8 +61,8 @@ export const buildTabs = (db) => { }, }); - const rawTab = fs.readFileSync(tabFile, 'utf-8'); - fs.writeFileSync(tabFile, convertRawTab(rawTab)); + const rawTab = await fs.readFile(tabFile, 'utf-8'); + await fs.writeFile(tabFile, convertRawTab(rawTab)); db.set(`tabs.${tabName}`, buildTime).write(); }; diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index 19dda62ca..4ec3c474a 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -7,8 +7,8 @@ import chalk from 'chalk'; import commonJS from 'rollup-plugin-commonjs'; import filesize from 'rollup-plugin-filesize'; import injectProcessEnv from 'rollup-plugin-inject-process-env'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; +import { join } from 'path'; +import { cjsDirname } from '../utilities'; import Low from 'lowdb'; import FileAsync from 'lowdb/adapters/FileAsync'; import { @@ -94,13 +94,6 @@ export function isFolderModified(relativeFolderPath, storedTimestamp) { return false; } -/** - * Function to replicate the functionality of `__dirname` in CJS code - */ -export function cjsDirname() { - return dirname(fileURLToPath(import.meta.url)); -} - /** * Get the path to the database file */ diff --git a/gulpfile.esm.js/index.js b/gulpfile.esm.js/index.js index e034df635..f8cf52eb1 100644 --- a/gulpfile.esm.js/index.js +++ b/gulpfile.esm.js/index.js @@ -1,7 +1,6 @@ /* eslint-disable import/no-named-as-default */ -import build from './build'; -import buildBundles from './build/bundles'; -import buildTabs from './build/tabs'; -import buildDocs from './build/docs'; - -export { build, buildBundles, buildTabs, buildDocs }; +export { default as build } from './build'; +export { default as buildBundles } from './build/bundles'; +export { default as buildTabs } from './build/tabs'; +export { default as buildDocs } from './build/docs'; +export { default as create } from './templates'; diff --git a/gulpfile.esm.js/templates/index.js b/gulpfile.esm.js/templates/index.js new file mode 100644 index 000000000..a0e539349 --- /dev/null +++ b/gulpfile.esm.js/templates/index.js @@ -0,0 +1,30 @@ +import { error as _error, info, rl, askQuestion, warn } from './print'; +import { addNew as addNewModule } from './module'; +import { addNew as addNewTab } from './tab'; + +async function askMode() { + while (true) { + // eslint-disable-next-line no-await-in-loop + const mode = await askQuestion( + 'What would you like to create? (module/tab)' + ); + if (mode !== 'module' && mode !== 'tab') { + warn("Please answer with only 'module' or 'tab'."); + } else { + return mode; + } + } +} + +export default async function () { + try { + const mode = await askMode(); + if (mode === 'module') await addNewModule(); + else if (mode === 'tab') await addNewTab(); + } catch (error) { + _error(`ERROR: ${error.message}`); + info('Terminating module app...'); + } finally { + rl.close(); + } +} diff --git a/gulpfile.esm.js/templates/utilities.js b/gulpfile.esm.js/templates/utilities.js new file mode 100644 index 000000000..f5289fd6b --- /dev/null +++ b/gulpfile.esm.js/templates/utilities.js @@ -0,0 +1,10 @@ +const snakeCaseRegex = /\b[a-z]+(?:_[a-z]+)*\b/u; +const pascalCaseRegex = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u; + +export function isSnakeCase(string) { + return snakeCaseRegex.test(string); +} + +export function isPascalCase(string) { + return pascalCaseRegex.test(string); +} diff --git a/gulpfile.esm.js/utilities.js b/gulpfile.esm.js/utilities.js new file mode 100644 index 000000000..7b4b1a2b9 --- /dev/null +++ b/gulpfile.esm.js/utilities.js @@ -0,0 +1,10 @@ +/** + * Utilities for scripts + */ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// eslint-disable-next-line import/prefer-default-export +export const cjsDirname = () => { + return dirname(fileURLToPath(import.meta.url)); +}; diff --git a/package.json b/package.json index 220a3a2c8..ea97fdda1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "Apache-2.0", "scripts-info": { "//NOTE": "Run `npm i npm-scripts-info -g` to install once globally, then run `npm-scripts-info` as needed to list these descriptions", - "module": "Interactively initialise a new bundle or tab from their templates", + "create": "Interactively initialise a new bundle or tab from their templates", "lint": "Check code for eslint warnings and errors", "lint:fix": "Lint code and fix automatically fixable problems", "build": "Lint code, then build modules and documentation", @@ -20,7 +20,7 @@ "test:watch": "Watch files for changes and rerun tests related to changed files" }, "scripts": { - "module": "node ./scripts/templates/app.js", + "create": "gulp create", "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", "build": "yarn lint && gulp build", From 53cb26dd008e820d7805c66de9c24b81fab68789 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 11 Aug 2022 02:46:12 +0800 Subject: [PATCH 16/51] Readded templates --- gulpfile.esm.js/templates/module.js | 39 ++++++++++ gulpfile.esm.js/templates/print.js | 29 ++++++++ gulpfile.esm.js/templates/tab.js | 66 +++++++++++++++++ .../templates/templates/__bundle__.ts | 34 +++++++++ .../templates/templates/__tab__.tsx | 71 +++++++++++++++++++ 5 files changed, 239 insertions(+) create mode 100644 gulpfile.esm.js/templates/module.js create mode 100644 gulpfile.esm.js/templates/print.js create mode 100644 gulpfile.esm.js/templates/tab.js create mode 100644 gulpfile.esm.js/templates/templates/__bundle__.ts create mode 100644 gulpfile.esm.js/templates/templates/__tab__.tsx diff --git a/gulpfile.esm.js/templates/module.js b/gulpfile.esm.js/templates/module.js new file mode 100644 index 000000000..3429dd0a4 --- /dev/null +++ b/gulpfile.esm.js/templates/module.js @@ -0,0 +1,39 @@ +import { promises as fs } from 'fs'; +import { askQuestion, warn, success } from './print'; +import { isSnakeCase } from './utilities'; +import manifest from '../../modules.json'; +import { cjsDirname } from '../utilities'; + +export function check(moduleName) { + return Object.keys(manifest).includes(moduleName); +} + +async function askModuleName() { + const name = await askQuestion( + 'What is the name of your new module? (eg. binary_tree)' + ); + if (isSnakeCase(name) === false) { + warn('Module names must be in snake case. (eg. binary_tree)'); + return askModuleName(); + } + if (check(name) === true) { + warn('A module with the same name already exists.'); + return askModuleName(); + } + return name; +} + +export async function addNew() { + const moduleName = await askModuleName(); + const bundleDestination = `src/bundles/${moduleName}`; + await fs.mkdir(bundleDestination, { recursive: true }); + await fs.copyFile( + `${cjsDirname()}/__bundle__.ts`, + `${bundleDestination}/index.ts` + ); + await fs.writeFile( + 'modules.json', + JSON.stringify({ ...manifest, [moduleName]: { tabs: [] } }, null, 2) + ); + success(`Bundle for module ${moduleName} created at ${bundleDestination}.`); +} diff --git a/gulpfile.esm.js/templates/print.js b/gulpfile.esm.js/templates/print.js new file mode 100644 index 000000000..1ce918dfc --- /dev/null +++ b/gulpfile.esm.js/templates/print.js @@ -0,0 +1,29 @@ +import { grey, red, yellow, green, blueBright } from 'chalk'; +import { createInterface } from 'readline'; + +export const rl = createInterface({ + input: process.stdin, + output: process.stdout, +}); + +export function info(...args) { + return console.log(...args.map((string) => grey(string))); +} + +export function error(...args) { + return console.log(...args.map((string) => red(string))); +} + +export function warn(...args) { + return console.log(...args.map((string) => yellow(string))); +} + +export function success(...args) { + return console.log(...args.map((string) => green(string))); +} + +export function askQuestion(question) { + return new Promise((resolve) => { + rl.question(blueBright(`${question}\n`), resolve); + }); +} diff --git a/gulpfile.esm.js/templates/tab.js b/gulpfile.esm.js/templates/tab.js new file mode 100644 index 000000000..ed94c2501 --- /dev/null +++ b/gulpfile.esm.js/templates/tab.js @@ -0,0 +1,66 @@ +/* eslint-disable no-await-in-loop */ +import { promises as fs } from 'fs'; +import { askQuestion, warn, success } from './print'; +import { isPascalCase } from './utilities'; +import { check as _check } from './module'; +import manifest from '../../modules.json'; +import { cjsDirname } from '../utilities'; + +const existingTabs = Object.values(manifest).flatMap((value) => value.tabs); + +export function check(tabName) { + return existingTabs.includes(tabName); +} + +async function askModuleName() { + while (true) { + const name = await askQuestion('Add a new tab to which module?'); + if (!_check(name)) { + warn(`Module ${name} does not exist.`); + } else { + return name; + } + } +} + +async function askTabName() { + while (true) { + const name = await askQuestion( + 'What is the name of your new tab? (eg. BinaryTree)' + ); + if (!isPascalCase(name)) { + warn('Tab names must be in pascal case. (eg. BinaryTree)'); + } else if (check(name)) { + warn('A tab with the same name already exists.'); + } else { + return name; + } + } +} + +export async function addNew() { + const moduleName = await askModuleName(); + const tabName = await askTabName(); + + // Copy module tab template into correct destination and show success message + const tabDestination = `src/tabs/${tabName}`; + await fs.mkdir(tabDestination, { recursive: true }); + await fs.copyFile( + `${cjsDirname()}/templates/__templates__.ts`, + `${tabDestination}/index.tsx` + ); + await fs.writeFile( + 'modules.json', + JSON.stringify( + { + ...manifest, + [moduleName]: { tabs: [...manifest[moduleName].tabs, tabName] }, + }, + null, + 2 + ) + ); + success( + `Tab ${tabName} for module ${moduleName} created at ${tabDestination}.` + ); +} diff --git a/gulpfile.esm.js/templates/templates/__bundle__.ts b/gulpfile.esm.js/templates/templates/__bundle__.ts new file mode 100644 index 000000000..8715efd53 --- /dev/null +++ b/gulpfile.esm.js/templates/templates/__bundle__.ts @@ -0,0 +1,34 @@ +/** + * A single sentence summarising the module (this sentence is displayed larger). + * + * Sentences describing the module. More sentences about the module. + * + * @module module_name + * @author Author Name + * @author Author Name + */ + +import { + ModuleContexts, + ModuleParams, +} from '../../../src/typings/type_helpers.js'; + +/** + * Sample function. Increments a number by 1. + * + * @param x The number to be incremented. + * @returns The incremented value of the number. + */ +function sample_function(x: number): number { + return ++x; +} + +//NOTE Remove the underscores before the parameter names if you will be using +// them. These parameters are passed in over on the frontend, and can later be +// accessed again in your module's tab via the DebuggerContext it gets passed +export default ( + _moduleParams: ModuleParams, + _moduleContexts: ModuleContexts +) => ({ + sample_function, +}); \ No newline at end of file diff --git a/gulpfile.esm.js/templates/templates/__tab__.tsx b/gulpfile.esm.js/templates/templates/__tab__.tsx new file mode 100644 index 000000000..34eb95b09 --- /dev/null +++ b/gulpfile.esm.js/templates/templates/__tab__.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +/** + * + * @author + * @author + */ + +/** + * React Component props for the Tab. + */ +type Props = { + children?: never; + className?: never; + context?: any; +}; + +/** + * React Component state for the Tab. + */ +type State = { + counter: number; +}; + +/** + * The main React Component of the Tab. + */ +class Repeat extends React.Component { + constructor(props) { + super(props); + this.state = { + counter: 0, + }; + } + + public render() { + const { counter } = this.state; + return ( +
    This is spawned from the repeat package. Counter is {counter}
    + ); + } +} + +export default { + /** + * This function will be called to determine if the component will be + * rendered. Currently spawns when the result in the REPL is "test". + * @param {DebuggerContext} context + * @returns {boolean} + */ + toSpawn: (context: any) => context.result.value === 'test', + + /** + * This function will be called to render the module tab in the side contents + * on Source Academy frontend. + * @param {DebuggerContext} context + */ + body: (context: any) => , + + /** + * The Tab's icon tooltip in the side contents on Source Academy frontend. + */ + label: 'Sample Tab', + + /** + * BlueprintJS IconName element's name, used to render the icon which will be + * displayed in the side contents panel. + * @see https://blueprintjs.com/docs/#icons + */ + iconName: 'build', +}; \ No newline at end of file From a5bc84325a7cd93653050b65e566657069b5251d Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 11 Aug 2022 02:46:21 +0800 Subject: [PATCH 17/51] Update .eslintrc.js --- .eslintrc.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 48d06accb..b6021c311 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -158,8 +158,12 @@ module.exports = { }, { files: ['./**/__tests__/**.test.js', './**/__tests__/**.test.ts'], - extends: ['airbnb', 'plugin:prettier/recommended', 'plugin:jest/recommeended'], - plugins: ['jest'] - } + extends: [ + 'airbnb', + 'plugin:prettier/recommended', + 'plugin:jest/recommended', + ], + plugins: ['jest'], + }, ], }; From 35b03cf468cf805bebd701bf94ca905ac0a9421d Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 11 Aug 2022 22:24:00 +0800 Subject: [PATCH 18/51] Fixed some template scripts --- gulpfile.esm.js/build/docs/index.js | 7 ++----- gulpfile.esm.js/index.js | 1 - gulpfile.esm.js/templates/module.js | 23 ++++++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index 9d8d1cb5a..1e1a27ba0 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -122,7 +122,7 @@ export const buildJsons = async (db) => { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}` ); - return undefined; + return [bundle, undefined]; } return [bundle, moduleDocs.children]; @@ -150,12 +150,9 @@ export const buildJsons = async (db) => { JSON.stringify(output, null, 2) ); - db.set(`docs.${bundle}`, buildTime); + db.set(`docs.${bundle}`, buildTime).write(); }) ); - - // Read from the TypeDoc output and retrieve the JSON relevant to the each module - await fs.rm('build/jsons/output', { recursive: true, force: true }); }; /** diff --git a/gulpfile.esm.js/index.js b/gulpfile.esm.js/index.js index f8cf52eb1..01115e326 100644 --- a/gulpfile.esm.js/index.js +++ b/gulpfile.esm.js/index.js @@ -1,4 +1,3 @@ -/* eslint-disable import/no-named-as-default */ export { default as build } from './build'; export { default as buildBundles } from './build/bundles'; export { default as buildTabs } from './build/tabs'; diff --git a/gulpfile.esm.js/templates/module.js b/gulpfile.esm.js/templates/module.js index 3429dd0a4..79e08d2d4 100644 --- a/gulpfile.esm.js/templates/module.js +++ b/gulpfile.esm.js/templates/module.js @@ -9,18 +9,19 @@ export function check(moduleName) { } async function askModuleName() { - const name = await askQuestion( - 'What is the name of your new module? (eg. binary_tree)' - ); - if (isSnakeCase(name) === false) { - warn('Module names must be in snake case. (eg. binary_tree)'); - return askModuleName(); - } - if (check(name) === true) { - warn('A module with the same name already exists.'); - return askModuleName(); + while (true) { + // eslint-disable-next-line no-await-in-loop + const name = await askQuestion( + 'What is the name of your new module? (eg. binary_tree)' + ); + if (isSnakeCase(name) === false) { + warn('Module names must be in snake case. (eg. binary_tree)'); + } else if (check(name)) { + warn('A module with the same name already exists.'); + } else { + return name; + } } - return name; } export async function addNew() { From cfb920393645848e3e7162033c58404c5b3e3322 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 11 Aug 2022 23:10:37 +0800 Subject: [PATCH 19/51] Separate docs and jsons in the database --- gulpfile.esm.js/build/docs/index.js | 13 +++++++------ gulpfile.esm.js/build/index.js | 2 +- gulpfile.esm.js/build/utilities.js | 10 +++++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/gulpfile.esm.js/build/docs/index.js b/gulpfile.esm.js/build/docs/index.js index 1e1a27ba0..6a67fb635 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/gulpfile.esm.js/build/docs/index.js @@ -79,7 +79,7 @@ const parsers = { */ export const buildJsons = async (db) => { const isBundleModifed = (bundle) => { - const timestamp = db.get(`docs.${bundle}`).value() || 0; + const timestamp = db.get(`jsons.${bundle}`).value() || 0; return isFolderModified(`src/bundles/${bundle}`, timestamp); }; @@ -108,7 +108,7 @@ export const buildJsons = async (db) => { const buildTime = new Date().getTime(); - const docsFile = await fs.readFile('build/docs.json'); + const docsFile = await fs.readFile('build/docs.json', 'utf-8'); const parsedJSON = JSON.parse(docsFile).children; if (!parsedJSON) { @@ -150,7 +150,7 @@ export const buildJsons = async (db) => { JSON.stringify(output, null, 2) ); - db.set(`docs.${bundle}`, buildTime).write(); + db.set(`jsons.${bundle}`, buildTime).write(); }) ); }; @@ -162,7 +162,7 @@ export const buildJsons = async (db) => { * their documentation won't be properly included. Hence all modules have to be built at * the same time. */ -export const buildDocs = async () => { +export const buildDocs = async (db) => { const app = new typedoc.Application(); app.options.addReader(new typedoc.TSConfigReader()); app.options.addReader(new typedoc.TypeDocReader()); @@ -189,6 +189,8 @@ export const buildDocs = async () => { `${cjsDirname()}/docs/README.md`, 'build/documentation/README.md' ); + + db.set('docs', new Date().getTime()).write(); } }; @@ -196,8 +198,7 @@ export const buildDocs = async () => { * Build both JSONS and HTML documentation */ export default async () => { - await buildDocs(); - const db = await getDb(); + await buildDocs(db); await buildJsons(db); }; diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js index dfcc9aee0..11563f6bf 100644 --- a/gulpfile.esm.js/build/index.js +++ b/gulpfile.esm.js/build/index.js @@ -12,7 +12,7 @@ export default series( await Promise.all([ buildBundles(db), buildTabs(db), - buildDocs().then(() => buildJsons(db)), + buildDocs(db).then(() => buildJsons(db)), ]); }, { diff --git a/gulpfile.esm.js/build/utilities.js b/gulpfile.esm.js/build/utilities.js index 4ec3c474a..e9d584769 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/gulpfile.esm.js/build/utilities.js @@ -105,6 +105,7 @@ export function getDbPath() { * Get a new Lowdb instance */ export function getDb() { + // eslint-disable-next-line new-cap return Low(new FileAsync(getDbPath())); } @@ -119,7 +120,10 @@ export function removeDuplicates(arr) { export const shouldBuildAll = (outputDir) => { if (process.argv[3] === '--force') return true; - if (!fs.existsSync(`build/${outputDir}`)) return true; - - return fs.readdirSync(`build/${outputDir}`).length === 0; + try { + return fs.readdirSync(`build/${outputDir}`).length === 0; + } catch (error) { + if (error.code === 'ENOENT') return true; + throw error; + } }; From d8196651719876e5a52e3a38cf457cbefe403d1e Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 12 Aug 2022 12:34:38 +0800 Subject: [PATCH 20/51] eslint config that actually works --- .eslintignore | 2 + .eslintrc.js | 169 ----- package.json | 9 +- scripts/.eslintrc.js | 32 + scripts/tsconfig.json | 19 + scripts/tsconfig.test.json | 3 + src/.eslintrc.js | 124 ++++ src/tsconfig.json | 4 +- yarn.lock | 1383 ++++++------------------------------ 9 files changed, 396 insertions(+), 1349 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 scripts/.eslintrc.js create mode 100644 scripts/tsconfig.json create mode 100644 scripts/tsconfig.test.json create mode 100644 src/.eslintrc.js diff --git a/.eslintignore b/.eslintignore index 27fa7672c..ea1058073 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ .github/ .husky/ build/ + +**/.eslintrc*.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b6021c311..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,169 +0,0 @@ -module.exports = { - extends: ['.eslintrc.base.js', 'plugin:prettier/recommended'], - - root: true, - parserOptions: { - sourceType: 'module', - }, - - rules: { - 'func-style': 0, - indent: [ - 1, - 2, // Was "tabs" - { - SwitchCase: 1, // Same - // VariableDeclarator: 1, - // outerIIFEBody: 1, - // MemberExpression: 1, - // FunctionDeclaration: { - // parameters: 1, - // body: 1 - // }, - // FunctionExpression: { - // parameters: 1, - // body: 1 - // }, - // StaticBlock: { - // body: 1 - // }, - // CallExpression: { - // arguments: 1, - // }, - // ArrayExpression: 1, - // ObjectExpression: 1, - // ImportDeclaration: 1, - // flatTernaryExpressions: false, - // offsetTernaryExpressions: false, - // ignoreComments: false - }, - ], - quotes: [ - 1, - 'single', // Was "double" - { - avoidEscape: true, // Same - // allowTemplateLiterals: false - }, - ], - - 'prettier/prettier': 1, // Was 2 - }, - - overrides: [ - { - files: ['*.ts', '*.tsx'], - - plugins: ['import', 'react', 'jsx-a11y', '@typescript-eslint'], - extends: ['airbnb-typescript', 'plugin:prettier/recommended'], - - parser: '@typescript-eslint/parser', - parserOptions: { - project: './src/tsconfig.json', - tsconfigRootDir: __dirname, - }, - - rules: { - // [typescript-eslint Extension Rules] - /* NOTE - .eslintrc.base.js has been configured for every rule off the - eslint:recommended config as of V8. - A similar complete config but for all typescript-eslint rules hasn't - been made, instead simply using airbnb-typescript's layers of - extended configs & plugins. - - This section is for reconfiguring the typescript-eslint extension - rules configured by airbnb-typescript that have replaced their eslint - equivalents, to make them match the behaviour in .eslintrc.base.js - */ - '@typescript-eslint/no-unused-vars': [ - 1, // Was 2 - { - // vars: "all", - // args: "after-used", - // ignoreRestSiblings: false, - argsIgnorePattern: '^_', - caughtErrors: 'all', // Was "none" - caughtErrorsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/no-use-before-define': [ - 1, // Was 2 - { - functions: false, - // classes: true, - // variables: true, - // enums: true, // TS - // typedefs: true, // TS - // ignoreTypeReferences: true, // TS - }, - ], - '@typescript-eslint/default-param-last': 1, // Was 2 - '@typescript-eslint/no-shadow': [ - 1, // Was 2 - { - builtinGlobals: true, - // hoist: "functions", - // ignoreTypeValueShadow: true, // TS - // ignoreFunctionTypeParameterNameValueShadow: true, // TS - }, - ], - '@typescript-eslint/lines-between-class-members': 0, // Was 2 - - // [Error → Warn] - /* NOTE - This section is for reducing the severity of rules configured by - airbnb-typescript from 2 to 1, if the problems they point out do not - have the possibility of directly leading to errors - */ - 'prettier/prettier': 1, - - // [Other] - '@typescript-eslint/naming-convention': [ - 1, - { - selector: 'variable', - // Was ['camelCase', 'PascalCase', 'UPPER_CASE']. - // Add snake case to let exported module variables match Source - format: ['camelCase', 'PascalCase', 'UPPER_CASE', 'snake_case'], - }, - { - selector: 'function', - // Was ['camelCase', 'PascalCase']. - // Add snake case to let exported module functions match Source - format: ['camelCase', 'PascalCase', 'snake_case'], - }, - { - selector: 'typeLike', - format: ['PascalCase'], - }, - ], - }, - }, - { - files: ['gulpfile.esm.js/**.js'], - extends: ['airbnb', 'plugin:prettier/recommended'], - parserOptions: { - ecmaVersion: '2020', - }, - rules: { - 'import/no-extraneous-dependencies': 0, - 'no-console': 0, - 'no-continue': 0, - 'no-param-reassign': 0, - 'no-restricted-syntax': 0, - 'prefer-const': 0, - 'prettier/prettier': 1, // Was 2 - }, - }, - { - files: ['./**/__tests__/**.test.js', './**/__tests__/**.test.ts'], - extends: [ - 'airbnb', - 'plugin:prettier/recommended', - 'plugin:jest/recommended', - ], - plugins: ['jest'], - }, - ], -}; diff --git a/package.json b/package.json index ea97fdda1..2b7a0041f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:watch": "Watch files for changes and rerun tests related to changed files" }, "scripts": { + "gulp": "cross-env TS_NODE_PROJECT=gulpfile.ts/tsconfig.json gulp", "create": "gulp create", "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", @@ -53,9 +54,9 @@ "@typescript-eslint/parser": "^5.18.0", "babel-jest": "^26.6.3", "babel-plugin-transform-import-meta": "^2.2.0", - "chalk": "^4.1.2", + "chalk": "^5.0.1", "cross-env": "^7.0.3", - "eslint": "^8.12.0", + "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", @@ -63,18 +64,15 @@ "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^26.8.1", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", "esm": "^3.2.25", "generate-template-files": "^3.0.0", - "gulp": "^4.0.2", "http-server": "^0.12.3", "husky": "5", "jest": "^26.6.3", "lowdb": "^1.0.0", "merge-stream": "^2.0.0", - "prettier": "^2.2.1", "rollup": "^2.41.2", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-commonjs": "^10.1.0", @@ -82,6 +80,7 @@ "rollup-plugin-filesize": "^9.1.1", "rollup-plugin-inject-process-env": "^1.3.1", "ts-jest": "^26.5.4", + "ts-node": "^10.9.1", "typedoc": "^0.20.33", "typescript": "^4.2.3", "yarnhook": "^0.5.1" diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 000000000..0a493058d --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,32 @@ +// Leaving everything double quoted so it's easier to switch between JS and JSON +// Since JSON has automatic schema validation + +module.exports = { + // Need react here because otherwise we get undefined rule errors + "plugins": ["import", "react", "@typescript-eslint"], + "extends": ["../.eslintrc.base.js", "airbnb-typescript"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "project": "./tsconfig.json", + "tsconfigRootDir": __dirname, + }, + "rules": { + "func-style": 0, + "import/no-extraneous-dependencies": 0, + "no-console": 0, + "no-continue": 0, + "no-param-reassign": 0, + "no-restricted-syntax": 0, + "prefer-const": 0 + }, + "overrides": [{ + "files": ["./**/__tests__/**.test.ts"], + "parserOptions": { + "project": "tsconfig.test.json", + "tsconfigRootDir": __dirname, + }, + "extends": ["plugin:jest/recommended"], + "plugins": ["jest"] + }] +} \ No newline at end of file diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..61e18c21f --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node", + "moduleTypes": { + "./**/*.ts": "esm", + }, + }, + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2020", + }, + "include": ["./**.ts"], + "exclude": ["./**/__tests__/**"] +} \ No newline at end of file diff --git a/scripts/tsconfig.test.json b/scripts/tsconfig.test.json new file mode 100644 index 000000000..62e0985f7 --- /dev/null +++ b/scripts/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "include": ["./**/__tests__/**.ts"], +} \ No newline at end of file diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 000000000..ab7770364 --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,124 @@ +module.exports = { + "extends": ["../.eslintrc.base.js", "airbnb-typescript"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "tsconfigRootDir": __dirname, + }, + "plugins": ["import", "react", "jsx-a11y", "@typescript-eslint"], + "rules": { + "func-style": 0, + "indent": [ + 1, + 2, // Was "tabs" + { + "SwitchCase": 1 // Same + // VariableDeclarator: 1, + // outerIIFEBody: 1, + // MemberExpression: 1, + // FunctionDeclaration: { + // parameters: 1, + // body: 1 + // }, + // FunctionExpression: { + // parameters: 1, + // body: 1 + // }, + // StaticBlock: { + // body: 1 + // }, + // CallExpression: { + // arguments: 1, + // }, + // ArrayExpression: 1, + // ObjectExpression: 1, + // ImportDeclaration: 1, + // flatTernaryExpressions: false, + // offsetTernaryExpressions: false, + // ignoreComments: false + } + ], + "quotes": [ + 1, + "single", // Was "double" + { + "avoidEscape": true // Same + // allowTemplateLiterals: false + } + ], + + // [typescript-eslint Extension Rules] + /* NOTE + .eslintrc.base.js has been configured for every rule off the + eslint:recommended config as of V8. + A similar complete config but for all typescript-eslint rules hasn"t + been made, instead simply using airbnb-typescript"s layers of + extended configs & plugins. + + This section is for reconfiguring the typescript-eslint extension + rules configured by airbnb-typescript that have replaced their eslint + equivalents, to make them match the behaviour in .eslintrc.base.js + */ + "@typescript-eslint/no-unused-vars": [ + 1, // Was 2 + { + // vars: "all", + // args: "after-used", + // ignoreRestSiblings: false, + "argsIgnorePattern": "^_", + "caughtErrors": "all", // Was "none" + "caughtErrorsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-use-before-define": [ + 1, // Was 2 + { + "functions": false + // classes: true, + // variables: true, + // enums: true, // TS + // typedefs: true, // TS + // ignoreTypeReferences: true, // TS + } + ], + "@typescript-eslint/default-param-last": 1, // Was 2 + "@typescript-eslint/no-shadow": [ + 1, // Was 2 + { + "builtinGlobals": true + // hoist: "functions", + // ignoreTypeValueShadow: true, // TS + // ignoreFunctionTypeParameterNameValueShadow: true, // TS + } + ], + "@typescript-eslint/lines-between-class-members": 0, // Was 2 + + // [Error → Warn] + /* NOTE + This section is for reducing the severity of rules configured by + airbnb-typescript from 2 to 1, if the problems they point out do not + have the possibility of directly leading to errors + */ + + // [Other] + "@typescript-eslint/naming-convention": [ + 1, + { + "selector": "variable", + // Was ["camelCase", "PascalCase", "UPPER_CASE"]. + // Add snake case to let exported module variables match Source + "format": ["camelCase", "PascalCase", "UPPER_CASE", "snake_case"] + }, + { + "selector": "function", + // Was ["camelCase", "PascalCase"]. + // Add snake case to let exported module functions match Source + "format": ["camelCase", "PascalCase", "snake_case"] + }, + { + "selector": "typeLike", + "format": ["PascalCase"] + } + ] + } +} \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json index aa9f62456..66c17ed9a 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -31,7 +31,7 @@ "noImplicitAny": false, }, /* Specifies an array of filenames or patterns to include in the program. These filenames are resolved relative to the directory containing the tsconfig.json file. */ - "include": ["."], + "include": [".", "../typings"], /* Specifies an array of filenames or patterns that should be skipped when resolving include. */ - "exclude": [], + "exclude": ["**/__tests__"], } diff --git a/yarn.lock b/yarn.lock index 7a07c2953..22033d93a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1174,30 +1174,42 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" + espree "^9.3.2" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" - minimatch "^3.0.4" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.9.2": - version "0.9.5" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" - integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== +"@humanwhocodes/config-array@^0.10.4": + version "0.10.4" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c" + integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" minimatch "^3.0.4" +"@humanwhocodes/gitignore-to-minimatch@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" + integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== + "@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" @@ -1403,6 +1415,24 @@ resolved "https://registry.yarnpkg.com/@joeychenofficial/alt-ergo-modified/-/alt-ergo-modified-2.4.0.tgz#27aec0cbed8ab4e2f0dad6feb4f0c9766ac3132f" integrity sha512-58b0K8pNUVZXGbua4IJQ+1K+E+jz3MkhDazZaaeKlD+sOLYR9iTHIbicV/I5K16ivYW6R9lONiT3dz8rMeFJ1w== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jscad/array-utils@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@jscad/array-utils/-/array-utils-2.1.3.tgz#0de2eaaf122c43efc534a948c8d80a65483160e8" @@ -1582,6 +1612,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.14" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.14.tgz#faaeefc4185ec71c389f4501ee5ec84b170cc402" @@ -1991,10 +2041,10 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-loose@^8.0.0: version "8.3.0" @@ -2008,7 +2058,7 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -2018,7 +2068,7 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.3, acorn@^8.5.0, acorn@^8.7.0: +acorn@^8.0.3, acorn@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== @@ -2028,6 +2078,11 @@ acorn@^8.0.5: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== +acorn@^8.4.1, acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -2069,13 +2124,6 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" - integrity sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA== - dependencies: - ansi-wrap "^0.1.0" - ansi-colors@^3.2.1: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" @@ -2088,13 +2136,6 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-gray@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" - integrity sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw== - dependencies: - ansi-wrap "0.1.0" - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -2139,11 +2180,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-wrap@0.1.0, ansi-wrap@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" - integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw== - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2168,23 +2204,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -append-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" - integrity sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA== - dependencies: - buffer-equal "^1.0.0" - aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== - are-we-there-yet@~1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" @@ -2193,6 +2217,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -2218,25 +2247,11 @@ arr-diff@^4.0.0: resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -arr-filter@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/arr-filter/-/arr-filter-1.1.2.tgz#43fdddd091e8ef11aa4c45d9cdc18e2dff1711ee" - integrity sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA== - dependencies: - make-iterator "^1.0.0" - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: +arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== -arr-map@^2.0.0, arr-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/arr-map/-/arr-map-2.0.2.tgz#3a77345ffc1cf35e2a91825601f9e58f2e24cac4" - integrity sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw== - dependencies: - make-iterator "^1.0.0" - arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" @@ -2247,11 +2262,6 @@ array-differ@^1.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= -array-each@^1.0.0, array-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" - integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA== - array-includes@^3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" @@ -2274,35 +2284,6 @@ array-includes@^3.1.4: get-intrinsic "^1.1.1" is-string "^1.0.7" -array-initial@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795" - integrity sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw== - dependencies: - array-slice "^1.0.0" - is-number "^4.0.0" - -array-last@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336" - integrity sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg== - dependencies: - is-number "^4.0.0" - -array-slice@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" - integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== - -array-sort@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a" - integrity sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg== - dependencies: - default-compare "^1.0.0" - get-value "^2.0.6" - kind-of "^5.0.2" - array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -2380,7 +2361,7 @@ astring@^1.4.3: resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.1.tgz#a91c4afd4af3523e11f31242a3d5d9af62bb6cc6" integrity sha512-Aj3mbwVzj7Vve4I/v2JYOPFkCGM2YS7OqQTNSxmUR+LECRpokuPgAYghePgr6SALDo5bD5DlfbSaYjOzGJZOLQ== -async-done@^1.2.0, async-done@^1.2.2, async-done@~1.3.2: +async-done@~1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== @@ -2395,13 +2376,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== -async-settle@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b" - integrity sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw== - dependencies: - async-done "^1.2.2" - async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -2544,21 +2518,6 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -bach@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" - integrity sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg== - dependencies: - arr-filter "^1.1.1" - arr-flatten "^1.0.1" - arr-map "^2.0.0" - array-each "^1.0.0" - array-initial "^1.0.0" - array-last "^1.1.1" - async-done "^1.2.2" - async-settle "^1.0.0" - now-and-later "^2.0.0" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -2707,11 +2666,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ== - buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2786,11 +2740,6 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg== - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2840,38 +2789,16 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" +chalk@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6" + integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -chokidar@^2.0.0: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - chokidar@^3.3.1: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -2952,15 +2879,6 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w== - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -2970,30 +2888,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g== - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - integrity sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag== - -clone@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== - -cloneable-readable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec" - integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ== - dependencies: - inherits "^2.0.1" - process-nextick-args "^2.0.0" - readable-stream "^2.3.5" - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3009,15 +2903,6 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== -collection-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-map/-/collection-map-1.0.0.tgz#aea0f06f8d26c780c2b75494385544b2255af18c" - integrity sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA== - dependencies: - arr-map "^2.0.2" - for-own "^1.0.0" - make-iterator "^1.0.0" - collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -3050,11 +2935,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - colorette@^1.1.0, colorette@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" @@ -3092,16 +2972,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - confusing-browser-globals@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" @@ -3119,26 +2989,11 @@ convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" -convert-source-map@^1.5.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -copy-props@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.5.tgz#03cf9ae328d4ebb36f8f1d804448a6af9ee3f2d2" - integrity sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw== - dependencies: - each-props "^1.3.2" - is-plain-object "^5.0.0" - core-js-compat@^3.8.1, core-js-compat@^3.9.0: version "3.9.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455" @@ -3162,6 +3017,11 @@ corser@^2.0.1: resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -3211,14 +3071,6 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== -d@1, d@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" - integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== - dependencies: - es5-ext "^0.10.50" - type "^1.0.1" - damerau-levenshtein@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" @@ -3273,7 +3125,7 @@ debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" -decamelize@^1.1.1, decamelize@^1.2.0: +decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3322,18 +3174,6 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -default-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" - integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== - dependencies: - kind-of "^5.0.2" - -default-resolution@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" - integrity sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ== - define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -3391,11 +3231,6 @@ depd@^1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== - detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -3416,6 +3251,11 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3466,24 +3306,6 @@ duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -each-props@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333" - integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA== - dependencies: - is-plain-object "^2.0.1" - object.defaults "^1.1.0" - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3539,7 +3361,7 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -3570,7 +3392,7 @@ errno@^0.1.2: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3634,42 +3456,6 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50: - version "0.10.62" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" - integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== - dependencies: - es6-iterator "^2.0.3" - es6-symbol "^3.1.3" - next-tick "^1.1.0" - -es6-iterator@^2.0.1, es6-iterator@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" - integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== - dependencies: - d "1" - es5-ext "^0.10.35" - es6-symbol "^3.1.1" - -es6-symbol@^3.1.1, es6-symbol@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" - integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== - dependencies: - d "^1.0.1" - ext "^1.1.2" - -es6-weak-map@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" - integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== - dependencies: - d "1" - es5-ext "^0.10.46" - es6-iterator "^2.0.3" - es6-symbol "^3.1.1" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3804,13 +3590,6 @@ eslint-plugin-jsx-a11y@^6.5.1: language-tags "^1.0.5" minimatch "^3.0.4" -eslint-plugin-prettier@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" - integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== - dependencies: - prettier-linter-helpers "^1.0.0" - eslint-plugin-react-hooks@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4" @@ -3874,13 +3653,14 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.12.0.tgz#c7a5bd1cfa09079aae64c9076c07eada66a46e8e" - integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q== +eslint@^8.21.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.21.0.tgz#1940a68d7e0573cef6f50037addee295ff9be9ef" + integrity sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA== dependencies: - "@eslint/eslintrc" "^1.2.1" - "@humanwhocodes/config-array" "^0.9.2" + "@eslint/eslintrc" "^1.3.0" + "@humanwhocodes/config-array" "^0.10.4" + "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -3890,14 +3670,17 @@ eslint@^8.12.0: eslint-scope "^7.1.1" eslint-utils "^3.0.0" eslint-visitor-keys "^3.3.0" - espree "^9.3.1" + espree "^9.3.3" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" + find-up "^5.0.0" functional-red-black-tree "^1.0.1" glob-parent "^6.0.1" - globals "^13.6.0" + globals "^13.15.0" + globby "^11.1.0" + grapheme-splitter "^1.0.4" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -3906,7 +3689,7 @@ eslint@^8.12.0: json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" regexpp "^3.2.0" @@ -3920,13 +3703,13 @@ esm@^3.2.25: resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== +espree@^9.3.2, espree@^9.3.3: + version "9.3.3" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d" + integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng== dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" + acorn "^8.8.0" + acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" esprima@^4.0.0, esprima@^4.0.1: @@ -4039,13 +3822,6 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== - dependencies: - homedir-polyfill "^1.0.1" - expect@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" @@ -4058,13 +3834,6 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" -ext@^1.1.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" - integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== - dependencies: - type "^2.5.0" - extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -4080,7 +3849,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -4109,26 +3878,11 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fancy-log@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" - integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== - dependencies: - ansi-gray "^0.1.1" - color-support "^1.1.3" - parse-node-version "^1.0.0" - time-stamp "^1.0.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - fast-glob@^3.0.3: version "3.2.5" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" @@ -4157,11 +3911,6 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz#e6a754cc8f15e58987aa9cbd27af66fd6f4e5af9" - integrity sha512-Ia0sQNrMPXXkqVFt6w6M1n1oKo3NfKs+mvaV811Jwir7vAk9a6PVV9VPYf6X3BU97QiLEmuW3uXH9u87zDFfdw== - fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -4220,14 +3969,6 @@ find-parent-dir@^0.3.0: resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ= -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA== - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4243,41 +3984,13 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" - integrity sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g== - dependencies: - detect-file "^1.0.0" - is-glob "^3.1.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -fined@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" - integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - expand-tilde "^2.0.2" - is-plain-object "^2.0.3" - object.defaults "^1.1.0" - object.pick "^1.2.0" - parse-filepath "^1.0.1" - -flagged-respawn@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" - integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + locate-path "^6.0.0" + path-exists "^4.0.0" flat-cache@^3.0.4: version "3.0.4" @@ -4292,31 +4005,16 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== -flush-write-stream@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - follow-redirects@^1.0.0: version "1.13.3" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== -for-in@^1.0.1, for-in@^1.0.2: +for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg== - dependencies: - for-in "^1.0.1" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -4369,14 +4067,6 @@ fs-minipass@^2.0.0, fs-minipass@^2.1.0: dependencies: minipass "^3.0.0" -fs-mkdirp-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" - integrity sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ== - dependencies: - graceful-fs "^4.1.11" - through2 "^2.0.3" - fs-readdir-recursive@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" @@ -4387,14 +4077,6 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - fsevents@^2.1.2, fsevents@~2.3.1, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -4442,11 +4124,6 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4560,35 +4237,6 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob-stream@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" - integrity sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw== - dependencies: - extend "^3.0.0" - glob "^7.1.1" - glob-parent "^3.1.0" - is-negated-glob "^1.0.0" - ordered-read-streams "^1.0.0" - pumpify "^1.3.5" - readable-stream "^2.1.5" - remove-trailing-separator "^1.0.1" - to-absolute-glob "^2.0.0" - unique-stream "^2.0.2" - -glob-watcher@^5.0.3: - version "5.0.5" - resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.5.tgz#aa6bce648332924d9a8489be41e3e5c52d4186dc" - integrity sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw== - dependencies: - anymatch "^2.0.0" - async-done "^1.2.0" - chokidar "^2.0.0" - is-negated-glob "^1.0.0" - just-debounce "^1.0.0" - normalize-path "^3.0.0" - object.defaults "^1.1.0" - glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4613,42 +4261,15 @@ glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0: - version "13.6.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.6.0.tgz#d77138e53738567bb96a3916ff6f6b487af20ef7" - integrity sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ== - dependencies: - type-fest "^0.20.2" - -globals@^13.9.0: - version "13.13.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" - integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== +globals@^13.15.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== dependencies: type-fest "^0.20.2" @@ -4690,13 +4311,6 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -glogg@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" - integrity sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA== - dependencies: - sparkles "^1.0.0" - glsl-tokenizer@^2.0.2: version "2.1.5" resolved "https://registry.yarnpkg.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz#1c2e78c16589933c274ba278d0a63b370c5fee1a" @@ -4720,16 +4334,21 @@ gpu.js@^2.10.4: gpu-mock.js "^1.3.0" webgpu "^0.1.16" -graceful-fs@^4.0.0, graceful-fs@^4.1.2, graceful-fs@^4.1.3: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - graceful-fs@^4.1.11, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.1.3: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -4740,47 +4359,6 @@ gud@^1.0.0: resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw== -gulp-cli@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" - integrity sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A== - dependencies: - ansi-colors "^1.0.1" - archy "^1.0.0" - array-sort "^1.0.0" - color-support "^1.1.3" - concat-stream "^1.6.0" - copy-props "^2.0.1" - fancy-log "^1.3.2" - gulplog "^1.0.0" - interpret "^1.4.0" - isobject "^3.0.1" - liftoff "^3.1.0" - matchdep "^2.0.0" - mute-stdout "^1.0.0" - pretty-hrtime "^1.0.0" - replace-homedir "^1.0.0" - semver-greatest-satisfied-range "^1.1.0" - v8flags "^3.2.0" - yargs "^7.1.0" - -gulp@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa" - integrity sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA== - dependencies: - glob-watcher "^5.0.3" - gulp-cli "^2.2.0" - undertaker "^1.2.1" - vinyl-fs "^3.0.0" - -gulplog@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" - integrity sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw== - dependencies: - glogg "^1.0.0" - gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -4893,13 +4471,6 @@ he@^1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" @@ -5072,7 +4643,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5082,7 +4653,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -ini@^1.3.4, ini@~1.3.0: +ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -5096,29 +4667,16 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -interpret@^1.0.0, interpret@^1.4.0: +interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ== - ip@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -is-absolute@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" - integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== - dependencies: - is-relative "^1.0.0" - is-windows "^1.0.1" - is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" @@ -5207,13 +4765,6 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" -is-core-module@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== - dependencies: - has "^1.0.3" - is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -5326,11 +4877,6 @@ is-module@^1.0.0: resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= -is-negated-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" - integrity sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug== - is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -5353,11 +4899,6 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -5382,7 +4923,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== @@ -5394,11 +4935,6 @@ is-plain-object@^3.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - is-potential-custom-element-name@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" @@ -5432,13 +4968,6 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-relative@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" - integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== - dependencies: - is-unc-path "^1.0.0" - is-shared-array-buffer@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -5480,23 +5009,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -is-unc-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" - integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== - dependencies: - unc-path-regex "^0.1.2" - -is-utf8@^0.2.0, is-utf8@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q== - -is-valid-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" - integrity sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA== - is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" @@ -5504,7 +5016,7 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -is-windows@^1.0.1, is-windows@^1.0.2: +is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== @@ -6163,11 +5675,6 @@ junk@^1.0.1: resolved "https://registry.yarnpkg.com/junk/-/junk-1.0.3.tgz#87be63488649cbdca6f53ab39bec9ccd2347f592" integrity sha1-h75jSIZJy9ym9Tqzm+yczSNH9ZI= -just-debounce@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.1.0.tgz#2f81a3ad4121a76bc7cb45dbf704c0d76a8e5ddf" - integrity sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ== - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -6182,7 +5689,7 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0, kind-of@^5.0.2: +kind-of@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== @@ -6209,35 +5716,6 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -last-run@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b" - integrity sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ== - dependencies: - default-resolution "^2.0.0" - es6-weak-map "^2.0.1" - -lazystream@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" - integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== - dependencies: - readable-stream "^2.0.5" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw== - dependencies: - invert-kv "^1.0.0" - -lead@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" - integrity sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow== - dependencies: - flush-write-stream "^1.0.2" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -6259,36 +5737,11 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -liftoff@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" - integrity sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog== - dependencies: - extend "^3.0.0" - findup-sync "^3.0.0" - fined "^1.0.1" - flagged-respawn "^1.0.0" - is-plain-object "^2.0.4" - object.map "^1.0.0" - rechoir "^0.6.2" - resolve "^1.1.7" - lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A== - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6304,6 +5757,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -6383,7 +5843,7 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -6409,13 +5869,6 @@ make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" -make-iterator@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" - integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== - dependencies: - kind-of "^6.0.2" - makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -6423,7 +5876,7 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-cache@^0.2.0, map-cache@^0.2.2: +map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= @@ -6440,16 +5893,6 @@ marked@^2.0.1: resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.1.tgz#5e7ed7009bfa5c95182e4eb696f85e948cefcee3" integrity sha512-5+/fKgMv2hARmMW7DOpykr2iLhl0NgjyELk5yn92iE7z8Se1IS9n3UsFm86hFXIkvMBmVxki8+ckcpjBeyo/hw== -matchdep@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e" - integrity sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA== - dependencies: - findup-sync "^2.0.0" - micromatch "^3.0.4" - resolve "^1.4.0" - stack-trace "0.0.10" - maximatch@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/maximatch/-/maximatch-0.1.0.tgz#86cd8d6b04c9f307c05a6b9419906d0360fb13a2" @@ -6470,7 +5913,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: +micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -6658,16 +6101,6 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mute-stdout@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" - integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== - -nan@^2.12.1: - version "2.16.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" - integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== - nan@^2.15.0: version "2.15.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" @@ -6705,11 +6138,6 @@ neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next-tick@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" - integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -6782,7 +6210,7 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: +normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -6809,13 +6237,6 @@ normalize.css@^8.0.1: resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3" integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg== -now-and-later@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" - integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== - dependencies: - once "^1.3.2" - npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" @@ -6960,7 +6381,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.0, object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -6970,16 +6391,6 @@ object.assign@^4.0.4, object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" -object.defaults@^1.0.0, object.defaults@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" - integrity sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA== - dependencies: - array-each "^1.0.1" - array-slice "^1.0.0" - for-own "^1.0.0" - isobject "^3.0.0" - object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -7006,29 +6417,13 @@ object.hasown@^1.1.0: define-properties "^1.1.3" es-abstract "^1.19.1" -object.map@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" - integrity sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w== - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - -object.pick@^1.2.0, object.pick@^1.3.0: +object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" -object.reduce@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.reduce/-/object.reduce-1.0.1.tgz#6fe348f2ac7fa0f95ca621226599096825bb03ad" - integrity sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw== - dependencies: - for-own "^1.0.0" - make-iterator "^1.0.0" - object.values@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" @@ -7088,20 +6483,6 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -ordered-read-streams@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" - integrity sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw== - dependencies: - readable-stream "^2.0.1" - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g== - dependencies: - lcid "^1.0.0" - p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -7126,6 +6507,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -7140,6 +6528,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -7189,22 +6584,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-filepath@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" - integrity sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q== - dependencies: - is-absolute "^1.0.0" - map-cache "^0.2.0" - path-root "^0.1.1" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== - dependencies: - error-ex "^1.2.0" - parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -7215,16 +6594,6 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse-node-version@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== - parse5@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" @@ -7245,13 +6614,6 @@ path-exists@4.0.0, path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ== - dependencies: - pinkie-promise "^2.0.0" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -7287,27 +6649,6 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== - dependencies: - path-root-regex "^0.1.0" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg== - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -7435,18 +6776,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" - integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== - pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -7466,11 +6795,6 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-hrtime@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== - process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -7542,14 +6866,6 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -7558,15 +6874,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.3.5: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -7673,14 +6980,6 @@ read-package-json-fast@^2.0.1: json-parse-even-better-errors "^2.3.0" npm-normalize-package-bin "^1.0.1" -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A== - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -7690,15 +6989,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ== - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - read-pkg@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" @@ -7728,7 +7018,7 @@ read-pkg@^5.2.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +readable-stream@^2.0.2, readable-stream@^2.0.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7869,24 +7159,7 @@ regl@2.1.0, regl@^2.1.0: resolved "https://registry.yarnpkg.com/regl/-/regl-2.1.0.tgz#7dae71e9ff20f29c4f42f510c70cd92ebb6b657c" integrity sha512-oWUce/aVoEvW5l2V0LK7O5KJMzUSKeiOwFuJehzpSFd43dO5spP9r+sSUfhKtsky4u6MCqWJaRL+abzExynfTg== -remove-bom-buffer@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" - integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== - dependencies: - is-buffer "^1.1.5" - is-utf8 "^0.2.1" - -remove-bom-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" - integrity sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA== - dependencies: - remove-bom-buffer "^3.0.0" - safe-buffer "^5.1.0" - through2 "^2.0.3" - -remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: +remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= @@ -7901,20 +7174,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -replace-ext@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" - integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== - -replace-homedir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c" - integrity sha512-CHPV/GAglbIB1tnQgaiysb8H2yCy8WQ7lcEwQ/eT+kLj0QHV8LnJW0zpqpE7RSkrMSRoa+EBoag86clf7WAgSg== - dependencies: - homedir-polyfill "^1.0.1" - is-absolute "^1.0.0" - remove-trailing-separator "^1.1.0" - replace-string@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/replace-string/-/replace-string-3.1.0.tgz#77a087d88580fbac59851237891aa4b0e283db72" @@ -7967,11 +7226,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug== - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -7994,14 +7248,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -8012,13 +7258,6 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-options@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" - integrity sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A== - dependencies: - value-or-function "^3.0.0" - resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -8032,15 +7271,6 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.14.2, resolve@^1.17 is-core-module "^2.2.0" path-parse "^1.0.6" -resolve@^1.1.7, resolve@^1.4.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - resolve@^1.20.0, resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" @@ -8164,7 +7394,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8221,13 +7451,6 @@ secure-compare@3.0.1: resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM= -semver-greatest-satisfied-range@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b" - integrity sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ== - dependencies: - sver-compat "^1.5.0" - "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -8476,11 +7699,6 @@ sourcemap-codec@^1.4.4: resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== -sparkles@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" - integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== - spdx-correct@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" @@ -8541,11 +7759,6 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" -stack-trace@0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" - integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== - stack-utils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" @@ -8578,11 +7791,6 @@ stream-exhaust@^1.0.1: resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -8591,7 +7799,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1, string-width@^1.0.2: +string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -8710,13 +7918,6 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g== - dependencies: - is-utf8 "^0.2.0" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -8774,14 +7975,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -sver-compat@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8" - integrity sha512-aFTHfmjwizMNlNE6dsGmoAM4lHjL0CyiobWaFiXWSlD7cIxshW422Nb8KbXCmR6z+0ZEPY+daXJrDyh/vuwTyg== - dependencies: - es6-iterator "^2.0.1" - es6-symbol "^3.1.1" - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -8856,14 +8049,6 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== -through2-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" - integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - through2@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" @@ -8879,32 +8064,11 @@ through2@^0.6.3: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" -through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -time-stamp@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" - integrity sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw== - tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= -to-absolute-glob@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" - integrity sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA== - dependencies: - is-absolute "^1.0.0" - is-negated-glob "^1.0.0" - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -8942,13 +8106,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -to-through@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" - integrity sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q== - dependencies: - through2 "^2.0.3" - tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -8989,6 +8146,25 @@ ts-jest@^26.5.4: semver "7.x" yargs-parser "20.x" +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -9077,16 +8253,6 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" - integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== - -type@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/type/-/type-2.7.0.tgz#aaff4ac90514e0dc1095b54af70505ef16cf00a2" - integrity sha512-NybX0NBIssNEj1efLf1mqKAtO4Q/Np5mqpa57be81ud7/tNHIXn48FDVXiyGMBF90FfXc5o7RPsuRQrPzgMOMA== - typed-styles@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" @@ -9099,11 +8265,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== - typedoc-default-themes@^0.12.10: version "0.12.10" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz#614c4222fe642657f37693ea62cad4dafeddf843" @@ -9161,32 +8322,6 @@ unbox-primitive@^1.0.1: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" -unc-path-regex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" - integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== - -undertaker-registry@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50" - integrity sha512-UR1khWeAjugW3548EfQmL9Z7pGMlBgXteQpr1IZeZBtnkCJQJIJ1Scj0mb9wQaPvUZ9Q17XqW6TIaPchJkyfqw== - -undertaker@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.3.0.tgz#363a6e541f27954d5791d6fa3c1d321666f86d18" - integrity sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg== - dependencies: - arr-flatten "^1.0.1" - arr-map "^2.0.0" - bach "^1.0.0" - collection-map "^1.0.0" - es6-weak-map "^2.0.1" - fast-levenshtein "^1.0.0" - last-run "^1.1.0" - object.defaults "^1.0.0" - object.reduce "^1.0.0" - undertaker-registry "^1.0.0" - unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -9241,14 +8376,6 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -unique-stream@^2.0.2: - version "2.3.1" - resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" - integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== - dependencies: - json-stable-stringify-without-jsonify "^1.0.1" - through2-filter "^3.0.0" - universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -9316,6 +8443,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -9330,13 +8462,6 @@ v8-to-istanbul@^7.0.0: convert-source-map "^1.6.0" source-map "^0.7.3" -v8flags@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.2.0.tgz#b243e3b4dfd731fa774e7492128109a0fe66d656" - integrity sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg== - dependencies: - homedir-polyfill "^1.0.1" - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -9352,11 +8477,6 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -value-or-function@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" - integrity sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg== - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -9366,54 +8486,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vinyl-fs@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" - integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== - dependencies: - fs-mkdirp-stream "^1.0.0" - glob-stream "^6.1.0" - graceful-fs "^4.0.0" - is-valid-glob "^1.0.0" - lazystream "^1.0.0" - lead "^1.0.0" - object.assign "^4.0.4" - pumpify "^1.3.5" - readable-stream "^2.3.3" - remove-bom-buffer "^3.0.0" - remove-bom-stream "^1.2.0" - resolve-options "^1.1.0" - through2 "^2.0.0" - to-through "^2.0.0" - value-or-function "^3.0.0" - vinyl "^2.0.0" - vinyl-sourcemap "^1.1.0" - -vinyl-sourcemap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" - integrity sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA== - dependencies: - append-buffer "^1.0.2" - convert-source-map "^1.5.0" - graceful-fs "^4.1.6" - normalize-path "^2.1.1" - now-and-later "^2.0.0" - remove-bom-buffer "^3.0.0" - vinyl "^2.0.0" - -vinyl@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.1.tgz#23cfb8bbab5ece3803aa2c0a1eb28af7cbba1974" - integrity sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw== - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - vscode-textmate@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.3.1.tgz#7ce578a4841a1b114485388ef734cc9e23b45a02" @@ -9494,11 +8566,6 @@ which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ== - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -9509,7 +8576,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== -which@^1.2.14, which@^1.2.9: +which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -9547,14 +8614,6 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw== - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" @@ -9610,16 +8669,11 @@ xmlhttprequest-ts@^1.0.1: dependencies: tslib "^1.9.2" -"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1: +"xtend@>=4.0.0 <4.1.0-0": version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - y18n@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" @@ -9648,14 +8702,6 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394" - integrity sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA== - dependencies: - camelcase "^3.0.0" - object.assign "^4.1.0" - yargs@15.3.1: version "15.3.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b" @@ -9690,25 +8736,6 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.2.tgz#63a0a5d42143879fdbb30370741374e0641d55db" - integrity sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA== - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.1" - yarnhook@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/yarnhook/-/yarnhook-0.5.1.tgz#27d47947412bd124874d56aa75725b76b68cc398" @@ -9716,3 +8743,13 @@ yarnhook@^0.5.1: dependencies: execa "^4.0.3" find-parent-dir "^0.3.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From bc72202cbd296257f1cfe9a6df1fb93e1fc17a75 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 12 Aug 2022 12:47:44 +0800 Subject: [PATCH 21/51] Bumped scripts to typescript Also removed gulp because getting it to work with typescript is way too painful --- .gitignore | 2 +- gulpfile.esm.js/build/index.js | 24 - gulpfile.esm.js/build/misc.js | 18 - gulpfile.esm.js/index.js | 5 - gulpfile.esm.js/templates/print.js | 29 -- gulpfile.esm.js/utilities.js | 10 - .../__tests__/tabs.test.ts | 36 +- .../build/buildUtils.ts | 270 +++++------ .../bundles.js => scripts/build/bundles.ts | 133 +++--- .../build/constants.ts | 24 +- .../build/docs/README.md | 0 .../build/docs/drawdown.ts | 0 .../index.js => scripts/build/docs/index.ts | 418 +++++++++--------- scripts/build/index.ts | 16 + scripts/build/misc.ts | 14 + .../build/tabs.js => scripts/build/tabs.ts | 166 ++++--- scripts/index.ts | 19 + .../index.js => scripts/templates/index.ts | 60 +-- .../module.js => scripts/templates/module.ts | 84 ++-- scripts/templates/print.ts | 29 ++ .../tab.js => scripts/templates/tab.ts | 133 +++--- .../templates/templates/__bundle__.ts | 0 .../templates/templates/__tab__.tsx | 0 .../templates/utilities.ts | 20 +- scripts/utilities.ts | 19 + src/bundles/binary_tree/functions.ts | 2 +- 26 files changed, 783 insertions(+), 748 deletions(-) delete mode 100644 gulpfile.esm.js/build/index.js delete mode 100644 gulpfile.esm.js/build/misc.js delete mode 100644 gulpfile.esm.js/index.js delete mode 100644 gulpfile.esm.js/templates/print.js delete mode 100644 gulpfile.esm.js/utilities.js rename gulpfile.esm.js/__tests__/tabs.test.js => scripts/__tests__/tabs.test.ts (62%) rename gulpfile.esm.js/build/utilities.js => scripts/build/buildUtils.ts (77%) rename gulpfile.esm.js/build/bundles.js => scripts/build/bundles.ts (63%) rename gulpfile.esm.js/build/constants.js => scripts/build/constants.ts (97%) rename {gulpfile.esm.js => scripts}/build/docs/README.md (100%) rename gulpfile.esm.js/build/docs/drawdown.js => scripts/build/docs/drawdown.ts (100%) rename gulpfile.esm.js/build/docs/index.js => scripts/build/docs/index.ts (74%) create mode 100644 scripts/build/index.ts create mode 100644 scripts/build/misc.ts rename gulpfile.esm.js/build/tabs.js => scripts/build/tabs.ts (61%) create mode 100644 scripts/index.ts rename gulpfile.esm.js/templates/index.js => scripts/templates/index.ts (90%) rename gulpfile.esm.js/templates/module.js => scripts/templates/module.ts (74%) create mode 100644 scripts/templates/print.ts rename gulpfile.esm.js/templates/tab.js => scripts/templates/tab.ts (82%) rename {gulpfile.esm.js => scripts}/templates/templates/__bundle__.ts (100%) rename {gulpfile.esm.js => scripts}/templates/templates/__tab__.tsx (100%) rename gulpfile.esm.js/templates/utilities.js => scripts/templates/utilities.ts (96%) create mode 100644 scripts/utilities.ts diff --git a/.gitignore b/.gitignore index 69972dd8b..828c32ffc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ build/* /package-lock.json coverage/ -gulpfile.esm.js/build/database.json +scripts/build/database.json # Compiled source # ################### diff --git a/gulpfile.esm.js/build/index.js b/gulpfile.esm.js/build/index.js deleted file mode 100644 index 11563f6bf..000000000 --- a/gulpfile.esm.js/build/index.js +++ /dev/null @@ -1,24 +0,0 @@ -import { series } from 'gulp'; -import copy from './misc'; -import { buildBundles } from './bundles'; -import { buildTabs } from './tabs'; -import { buildDocs, buildJsons } from './docs'; -import { getDb } from './utilities'; - -export default series( - Object.assign( - async () => { - const db = await getDb(); - await Promise.all([ - buildBundles(db), - buildTabs(db), - buildDocs(db).then(() => buildJsons(db)), - ]); - }, - { - displayName: 'build', - description: 'Build bundles, tabs and documentation', - } - ), - copy -); diff --git a/gulpfile.esm.js/build/misc.js b/gulpfile.esm.js/build/misc.js deleted file mode 100644 index 6b1cb3f18..000000000 --- a/gulpfile.esm.js/build/misc.js +++ /dev/null @@ -1,18 +0,0 @@ -import gulp, { series } from 'gulp'; -import { getDbPath } from './utilities'; - -/** - * Copy `modules.json` to the build folder - */ -export const copyModules = () => - gulp.src('modules.json').pipe(gulp.dest('build/')); - -/** - * Copy `database.json` to the build folder - */ -export const copyDatabase = () => - gulp.src(getDbPath()).pipe(gulp.dest('build/')); - -export default Object.assign(series(copyModules, copyDatabase), { - displayName: 'copy', -}); diff --git a/gulpfile.esm.js/index.js b/gulpfile.esm.js/index.js deleted file mode 100644 index 01115e326..000000000 --- a/gulpfile.esm.js/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as build } from './build'; -export { default as buildBundles } from './build/bundles'; -export { default as buildTabs } from './build/tabs'; -export { default as buildDocs } from './build/docs'; -export { default as create } from './templates'; diff --git a/gulpfile.esm.js/templates/print.js b/gulpfile.esm.js/templates/print.js deleted file mode 100644 index 1ce918dfc..000000000 --- a/gulpfile.esm.js/templates/print.js +++ /dev/null @@ -1,29 +0,0 @@ -import { grey, red, yellow, green, blueBright } from 'chalk'; -import { createInterface } from 'readline'; - -export const rl = createInterface({ - input: process.stdin, - output: process.stdout, -}); - -export function info(...args) { - return console.log(...args.map((string) => grey(string))); -} - -export function error(...args) { - return console.log(...args.map((string) => red(string))); -} - -export function warn(...args) { - return console.log(...args.map((string) => yellow(string))); -} - -export function success(...args) { - return console.log(...args.map((string) => green(string))); -} - -export function askQuestion(question) { - return new Promise((resolve) => { - rl.question(blueBright(`${question}\n`), resolve); - }); -} diff --git a/gulpfile.esm.js/utilities.js b/gulpfile.esm.js/utilities.js deleted file mode 100644 index 7b4b1a2b9..000000000 --- a/gulpfile.esm.js/utilities.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Utilities for scripts - */ -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -// eslint-disable-next-line import/prefer-default-export -export const cjsDirname = () => { - return dirname(fileURLToPath(import.meta.url)); -}; diff --git a/gulpfile.esm.js/__tests__/tabs.test.js b/scripts/__tests__/tabs.test.ts similarity index 62% rename from gulpfile.esm.js/__tests__/tabs.test.js rename to scripts/__tests__/tabs.test.ts index f663d5349..c17b30866 100644 --- a/gulpfile.esm.js/__tests__/tabs.test.js +++ b/scripts/__tests__/tabs.test.ts @@ -1,17 +1,19 @@ -import { convertRawTab } from "../build/tabs"; - -describe('Testing raw tab processing', () => { - test('Converts React tab properly', () => { - const rawTab = '(function (React) {}(React))'; - const result = convertRawTab(rawTab); - - expect(result).toEqual('(function (React) {})'); - }) - - test('Converts ReactDOM tab properly', () => { - const rawTab = '(function (React, ReactDOM) {}(React, ReactDOM))'; - const result = convertRawTab(rawTab); - - expect(result).toEqual('(function (React, ReactDOM) {})'); - }) -}) \ No newline at end of file +import { convertRawTab } from '../build/tabs'; + +describe('Testing raw tab processing', () => { + test('Converts React tab properly', () => { + const rawTab = '(function (React) {}(React))'; + const result = convertRawTab(rawTab); + + expect(result) + .toEqual('(function (React) {})'); + }); + + test('Converts ReactDOM tab properly', () => { + const rawTab = '(function (React, ReactDOM) {}(React, ReactDOM))'; + const result = convertRawTab(rawTab); + + expect(result) + .toEqual('(function (React, ReactDOM) {})'); + }); +}); diff --git a/gulpfile.esm.js/build/utilities.js b/scripts/build/buildUtils.ts similarity index 77% rename from gulpfile.esm.js/build/utilities.js rename to scripts/build/buildUtils.ts index e9d584769..835c9d595 100644 --- a/gulpfile.esm.js/build/utilities.js +++ b/scripts/build/buildUtils.ts @@ -1,129 +1,141 @@ -// /* [Imports] */ -import fs from 'fs'; -import babel from '@rollup/plugin-babel'; -import rollupResolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; -import chalk from 'chalk'; -import commonJS from 'rollup-plugin-commonjs'; -import filesize from 'rollup-plugin-filesize'; -import injectProcessEnv from 'rollup-plugin-inject-process-env'; -import { join } from 'path'; -import { cjsDirname } from '../utilities'; -import Low from 'lowdb'; -import FileAsync from 'lowdb/adapters/FileAsync'; -import { - DATABASE_NAME, - NODE_MODULES_PATTERN, - SOURCE_PATTERN, - SUPPRESSED_WARNINGS, -} from './constants'; - -/** - * Default configuration used by rollup for transpiling both tabs and bundles - */ -export const defaultConfig = { - onwarn(warning, warn) { - if (SUPPRESSED_WARNINGS.includes(warning.code)) return; - - warn(warning); - }, - plugins: [ - typescript({ - tsconfig: 'src/tsconfig.json', - }), - babel({ - babelHelpers: 'bundled', - extensions: ['.ts', '.tsx'], - include: [SOURCE_PATTERN], - }), - rollupResolve({ - // Source Academy's modules run in a browser environment. The default setting (false) may - // cause compilation issues when using some imported packages. - // https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser - browser: true, - // Tells rollup to look for locally installed modules instead of preferring built-in ones. - // Node's built-in modules include `fs` and `path`, which the jsdom browser environment does - // not have. - // https://github.com/rollup/plugins/tree/master/packages/node-resolve#preferbuiltins - preferBuiltins: false, - }), - commonJS({ - include: NODE_MODULES_PATTERN, - }), - injectProcessEnv({ - NODE_ENV: process.env.NODE_ENV, - }), - - filesize({ - showMinifiedSize: false, - showGzippedSize: false, - }), - ], -}; - -// Function takes in relative paths, for cleaner logging -export function isFolderModified(relativeFolderPath, storedTimestamp) { - function toFullPath(rootRelativePath) { - return join(process.cwd(), rootRelativePath); - } - - let fullFolderPath = toFullPath(relativeFolderPath); - - let contents = fs.readdirSync(fullFolderPath); - for (let content of contents) { - let relativeContentPath = join(relativeFolderPath, content); - let fullContentPath = join(fullFolderPath, content); - - let stats = fs.statSync(fullContentPath); - - // If is folder, recurse. If found something modified, stop early - if ( - stats.isDirectory() && - isFolderModified(relativeContentPath, storedTimestamp) - ) { - return true; - } - - // Is file. Compare timestamps to see if stop early - if (stats.mtimeMs > storedTimestamp) { - console.log(chalk.grey(`• File modified: ${relativeContentPath}`)); - return true; - } - } - - return false; -} - -/** - * Get the path to the database file - */ -export function getDbPath() { - return join(cjsDirname(), `${DATABASE_NAME}.json`); -} - -/** - * Get a new Lowdb instance - */ -export function getDb() { - // eslint-disable-next-line new-cap - return Low(new FileAsync(getDbPath())); -} - -export function removeDuplicates(arr) { - return [...new Set(arr)]; -} - -/** - * Checks if the given output directory is empty, to determine - * if the given build script should execute regardless of the last build time - */ -export const shouldBuildAll = (outputDir) => { - if (process.argv[3] === '--force') return true; - - try { - return fs.readdirSync(`build/${outputDir}`).length === 0; - } catch (error) { - if (error.code === 'ENOENT') return true; - throw error; - } -}; +// /* [Imports] */ +import fs from 'fs'; +import { babel } from '@rollup/plugin-babel'; +import rollupResolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; +import chalk from 'chalk'; +import commonJS from 'rollup-plugin-commonjs'; +import filesize from 'rollup-plugin-filesize'; +import injectProcessEnv from 'rollup-plugin-inject-process-env'; +import { join } from 'path'; +import Low from 'lowdb'; +import FileAsync from 'lowdb/adapters/FileAsync'; +import { + DATABASE_NAME, + NODE_MODULES_PATTERN, + SOURCE_PATTERN, + SUPPRESSED_WARNINGS, +} from './constants'; +import { cjsDirname } from '../utilities'; + +/** + * Default configuration used by rollup for transpiling both tabs and bundles + */ +export const defaultConfig = { + onwarn(warning: any, warn: any) { + if (SUPPRESSED_WARNINGS.includes(warning.code)) return; + + warn(warning); + }, + plugins: [ + typescript({ + tsconfig: 'src/tsconfig.json', + }), + babel({ + babelHelpers: 'bundled', + extensions: ['.ts', '.tsx'], + include: [SOURCE_PATTERN], + }), + rollupResolve({ + // Source Academy's modules run in a browser environment. The default setting (false) may + // cause compilation issues when using some imported packages. + // https://github.com/rollup/plugins/tree/master/packages/node-resolve#browser + browser: true, + // Tells rollup to look for locally installed modules instead of preferring built-in ones. + // Node's built-in modules include `fs` and `path`, which the jsdom browser environment does + // not have. + // https://github.com/rollup/plugins/tree/master/packages/node-resolve#preferbuiltins + preferBuiltins: false, + }), + commonJS({ + include: NODE_MODULES_PATTERN, + }), + injectProcessEnv({ + NODE_ENV: process.env.NODE_ENV, + }), + + filesize({ + showMinifiedSize: false, + showGzippedSize: false, + }), + ], +}; + +// Function takes in relative paths, for cleaner logging +export function isFolderModified(relativeFolderPath: string, storedTimestamp: number) { + function toFullPath(rootRelativePath: string) { + return join(process.cwd(), rootRelativePath); + } + + let fullFolderPath = toFullPath(relativeFolderPath); + + let contents = fs.readdirSync(fullFolderPath); + for (let content of contents) { + let relativeContentPath = join(relativeFolderPath, content); + let fullContentPath = join(fullFolderPath, content); + + let stats = fs.statSync(fullContentPath); + + // If is folder, recurse. If found something modified, stop early + if ( + stats.isDirectory() + && isFolderModified(relativeContentPath, storedTimestamp) + ) { + return true; + } + + // Is file. Compare timestamps to see if stop early + if (stats.mtimeMs > storedTimestamp) { + console.log(chalk.grey(`• File modified: ${relativeContentPath}`)); + return true; + } + } + + return false; +} + +/** + * Get the path to the database file + */ +export function getDbPath() { + return join(cjsDirname(import.meta.url), `${DATABASE_NAME}.json`); +} + +type DBKeys = 'jsons' | 'bundles' | 'tabs'; + +export type DBType = { + docs: number; +} & { + [k in DBKeys]: { + [name: string]: number; + } +}; + +/** + * Get a new Lowdb instance + */ +export function getDb() { + // eslint-disable-next-line new-cap + return Low(new FileAsync(getDbPath())); +} + +export type BuildTask = (db: Low.LowdbAsync) => Promise; + +export function removeDuplicates(arr: T[]) { + return [...new Set(arr)]; +} + +/** + * Checks if the given output directory is empty, to determine + * if the given build script should execute regardless of the last build time + */ +export const shouldBuildAll = (outputDir: string) => { + if (process.argv[3] === '--force') return true; + + try { + return fs.readdirSync(`build/${outputDir}`).length === 0; + } catch (error) { + if (error.code === 'ENOENT') return true; + throw error; + } +}; diff --git a/gulpfile.esm.js/build/bundles.js b/scripts/build/bundles.ts similarity index 63% rename from gulpfile.esm.js/build/bundles.js rename to scripts/build/bundles.ts index a3b4f2139..2ecc46364 100644 --- a/gulpfile.esm.js/build/bundles.js +++ b/scripts/build/bundles.ts @@ -1,68 +1,65 @@ -import chalk from 'chalk'; -import gulp from 'gulp'; -import { rollup } from 'rollup'; -import modules from '../../modules.json'; -import copy from './misc'; -import { - defaultConfig, - getDb, - isFolderModified, - shouldBuildAll, -} from './utilities'; - -/** - * Transpile bundles to the build folder - */ -export const buildBundles = (db) => { - const isBundleModifed = (bundle) => { - if (process.argv[3] === '--force') return true; - const timestamp = db.get(`bundles.${bundle}`).value() || 0; - return isFolderModified(`src/bundles/${bundle}`, timestamp); - }; - - const bundleNames = Object.keys(modules); - const filteredBundles = shouldBuildAll('bundles') - ? bundleNames - : bundleNames.filter(isBundleModifed); - - if (filteredBundles.length === 0) { - console.log('All bundles up to date'); - return null; - } - - console.log(chalk.greenBright('Building bundles for the following modules:')); - console.log( - filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') - ); - - const buildTime = new Date().getTime(); - - const processBundle = async (bundle) => { - const result = await rollup({ - ...defaultConfig, - input: `src/bundles/${bundle}/index.ts`, - }); - - await result.write({ - file: `build/bundles/${bundle}.js`, - format: 'iife', - }); - - db.set(`bundle.${bundle}`, buildTime).write(); - }; - - return Promise.all(filteredBundles.map(processBundle)); -}; - -export default gulp.series( - Object.assign( - async () => { - const db = await getDb(); - await buildBundles(db); - }, - { - displayName: 'buildBundles', - } - ), - copy -); +import chalk from 'chalk'; +import { rollup } from 'rollup'; +import { modules } from '../utilities'; +import copy from './misc'; +import { + BuildTask, + defaultConfig, + getDb, + isFolderModified, + shouldBuildAll, +} from './buildUtils'; + +/** + * Transpile bundles to the build folder + */ +export const buildBundles: BuildTask = (db) => { + const isBundleModifed = (bundle: string) => { + if (process.argv[3] === '--force') return true; + const timestamp = db.get(`bundles.${bundle}`) + .value() || 0; + return isFolderModified(`src/bundles/${bundle}`, timestamp); + }; + + const bundleNames = Object.keys(modules); + const filteredBundles = shouldBuildAll('bundles') + ? bundleNames + : bundleNames.filter(isBundleModifed); + + if (filteredBundles.length === 0) { + console.log('All bundles up to date'); + return null; + } + + console.log(chalk.greenBright('Building bundles for the following modules:')); + console.log( + filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`) + .join('\n'), + ); + + const buildTime = new Date() + .getTime(); + + const processBundle = async (bundle: string) => { + const result = await rollup({ + ...defaultConfig, + input: `src/bundles/${bundle}/index.ts`, + }); + + await result.write({ + file: `build/bundles/${bundle}.js`, + format: 'iife', + }); + + db.set(`bundle.${bundle}`, buildTime) + .write(); + }; + + return Promise.all(filteredBundles.map(processBundle)); +}; + +export default async () => { + const db = await getDb(); + await buildBundles(db); + await copy(); +}; diff --git a/gulpfile.esm.js/build/constants.js b/scripts/build/constants.ts similarity index 97% rename from gulpfile.esm.js/build/constants.js rename to scripts/build/constants.ts index 52758dfc1..edc898a77 100644 --- a/gulpfile.esm.js/build/constants.js +++ b/scripts/build/constants.ts @@ -1,12 +1,12 @@ -/* [Exports] */ -export const SUPPRESSED_WARNINGS = ['MISSING_NAME_OPTION_FOR_IIFE_EXPORT']; - -export const DATABASE_NAME = 'database'; -export const DATABASE_KEY = 'timestamp'; - -export const SOURCE_PATH = './src/'; -export const BUILD_PATH = './build/'; - -export const SOURCE_PATTERN = `${SOURCE_PATH}**`; -export const NODE_MODULES_PATTERN = './node_modules/**'; -export const MODULES_PATH = './modules.json'; +/* [Exports] */ +export const SUPPRESSED_WARNINGS = ['MISSING_NAME_OPTION_FOR_IIFE_EXPORT']; + +export const DATABASE_NAME = 'database'; +export const DATABASE_KEY = 'timestamp'; + +export const SOURCE_PATH = './src/'; +export const BUILD_PATH = './build/'; + +export const SOURCE_PATTERN = `${SOURCE_PATH}**`; +export const NODE_MODULES_PATTERN = './node_modules/**'; +export const MODULES_PATH = './modules.json'; diff --git a/gulpfile.esm.js/build/docs/README.md b/scripts/build/docs/README.md similarity index 100% rename from gulpfile.esm.js/build/docs/README.md rename to scripts/build/docs/README.md diff --git a/gulpfile.esm.js/build/docs/drawdown.js b/scripts/build/docs/drawdown.ts similarity index 100% rename from gulpfile.esm.js/build/docs/drawdown.js rename to scripts/build/docs/drawdown.ts diff --git a/gulpfile.esm.js/build/docs/index.js b/scripts/build/docs/index.ts similarity index 74% rename from gulpfile.esm.js/build/docs/index.js rename to scripts/build/docs/index.ts index 6a67fb635..69c0306c0 100644 --- a/gulpfile.esm.js/build/docs/index.js +++ b/scripts/build/docs/index.ts @@ -1,204 +1,214 @@ -import { constants as fsConstants, promises as fs } from 'fs'; -import * as typedoc from 'typedoc'; -import chalk from 'chalk'; -import drawdown from './drawdown'; -import { - isFolderModified, - getDb, - cjsDirname, - shouldBuildAll, -} from '../utilities'; -import modules from '../../../modules.json'; - -/** - * Convert each element type (e.g. variable, function) to its respective HTML docstring - * to be displayed to users - */ -const parsers = { - Variable(element, bundle) { - let desc; - if (element.comment && element.comment.shortText) { - desc = drawdown(element.comment.shortText); - } else { - desc = element.name; - console.warn( - `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ - element.name - }` - ); - } - - const typeStr = - element.type.name && element.type.name ? `:${element.type.name}` : ''; - - return `

    ${element.name}${typeStr}

    ${desc}
    `; - }, - Function(element, bundle) { - if (!element.signatures || element.signatures[0] === undefined) - throw new Error( - `Error: ${bundle}: Unable to find a signature for function ${element.name}!` - ); - - // In source all functions should only have one signature - const signature = element.signatures[0]; - - // Form the parameter string for the function - let paramStr; - if (!signature.parameters) paramStr = '()'; - else - paramStr = `(${signature.parameters - .map((param) => { - const typeStr = param.type ? param.type.name : 'unknown'; - return `${param.name}: ${typeStr}`; - }) - .join(', ')})`; - - // Form the result representation for the function - let resultStr; - if (!signature.type) resultStr = 'void'; - else resultStr = signature.type.name; - - let desc; - if (signature.comment && signature.comment.shortText) { - desc = drawdown(signature.comment.shortText); - } else { - desc = element.name; - console.warn( - `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ - element.name - }` - ); - } - - return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; - }, -}; - -/** - * Build the json documentation for the specified modules - */ -export const buildJsons = async (db) => { - const isBundleModifed = (bundle) => { - const timestamp = db.get(`jsons.${bundle}`).value() || 0; - return isFolderModified(`src/bundles/${bundle}`, timestamp); - }; - - const bundleNames = Object.keys(modules); - const filteredBundles = shouldBuildAll('jsons') - ? bundleNames - : bundleNames.filter(isBundleModifed); - - if (filteredBundles.length === 0) { - console.log('Documentation up to date'); - return; - } - - console.log( - chalk.greenBright('Building documentation for the following bundles:') - ); - console.log( - filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`).join('\n') - ); - - try { - await fs.access('build/jsons', fsConstants.F_OK); - } catch (_error) { - await fs.mkdir('build/jsons'); - } - - const buildTime = new Date().getTime(); - - const docsFile = await fs.readFile('build/docs.json', 'utf-8'); - const parsedJSON = JSON.parse(docsFile).children; - - if (!parsedJSON) { - throw new Error('Failed to parse docs.json'); - } - - const bundles = Object.keys(modules).map((bundle) => { - const moduleDocs = parsedJSON.find((x) => x.name === bundle); - - if (!moduleDocs || !moduleDocs.children) { - console.warn( - `${chalk.yellow('Warning:')} No documentation found for ${bundle}` - ); - return [bundle, undefined]; - } - - return [bundle, moduleDocs.children]; - }); - - await Promise.all( - bundles.map(async ([bundle, docs]) => { - if (!docs) return; - - const output = {}; - docs.forEach((element) => { - if (parsers[element.kindString]) { - output[element.name] = parsers[element.kindString](element, bundle); - } else { - console.warn( - `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ - element.name - } of type ${element.type}` - ); - } - }); - - await fs.writeFile( - `build/jsons/${bundle}.json`, - JSON.stringify(output, null, 2) - ); - - db.set(`jsons.${bundle}`, buildTime).write(); - }) - ); -}; - -/** - * Build the HTML documentation for all modules.\ - * \ - * TypeDoc always clears the directory after each build, so if you leave some modules out - * their documentation won't be properly included. Hence all modules have to be built at - * the same time. - */ -export const buildDocs = async (db) => { - const app = new typedoc.Application(); - app.options.addReader(new typedoc.TSConfigReader()); - app.options.addReader(new typedoc.TypeDocReader()); - - app.bootstrap({ - entryPoints: Object.keys(modules).map( - (bundle) => `src/bundles/${bundle}/functions.ts` - ), - tsconfig: 'src/tsconfig.json', - theme: 'typedoc-modules-theme', - excludeInternal: true, - categorizeByGroup: true, - name: 'Source Academy Modules', - }); - - const project = app.convert(); - if (project) { - const docsTask = app.generateDocs(project, 'build/documentation'); - const jsonTask = app.generateJson(project, 'build/docs.json'); - await Promise.all([docsTask, jsonTask]); - - // For some reason typedoc's not working, so do a manual copy - await fs.copyFile( - `${cjsDirname()}/docs/README.md`, - 'build/documentation/README.md' - ); - - db.set('docs', new Date().getTime()).write(); - } -}; - -/** - * Build both JSONS and HTML documentation - */ -export default async () => { - const db = await getDb(); - await buildDocs(db); - await buildJsons(db); -}; +import { constants as fsConstants, promises as fs } from 'fs'; +import * as typedoc from 'typedoc'; +import chalk from 'chalk'; +import drawdown from './drawdown'; +import { + isFolderModified, + getDb, + shouldBuildAll, + BuildTask, +} from '../buildUtils'; +import { cjsDirname, modules } from '../../utilities'; + +/** + * Convert each element type (e.g. variable, function) to its respective HTML docstring + * to be displayed to users + */ +const parsers = { + Variable(element, bundle) { + let desc; + if (element.comment && element.comment.shortText) { + desc = drawdown(element.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ + element.name + }`, + ); + } + + const typeStr + = element.type.name && element.type.name ? `:${element.type.name}` : ''; + + return `

    ${element.name}${typeStr}

    ${desc}
    `; + }, + Function(element, bundle) { + if (!element.signatures || element.signatures[0] === undefined) { + throw new Error( + `Error: ${bundle}: Unable to find a signature for function ${element.name}!`, + ); + } + + // In source all functions should only have one signature + const signature = element.signatures[0]; + + // Form the parameter string for the function + let paramStr; + if (!signature.parameters) paramStr = '()'; + else { + paramStr = `(${signature.parameters + .map((param) => { + const typeStr = param.type ? param.type.name : 'unknown'; + return `${param.name}: ${typeStr}`; + }) + .join(', ')})`; + } + + // Form the result representation for the function + let resultStr; + if (!signature.type) resultStr = 'void'; + else resultStr = signature.type.name; + + let desc; + if (signature.comment && signature.comment.shortText) { + desc = drawdown(signature.comment.shortText); + } else { + desc = element.name; + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No description found for ${ + element.name + }`, + ); + } + + return `

    ${element.name}${paramStr} → {${resultStr}}

    ${desc}
    `; + }, +}; + +/** + * Build the json documentation for the specified modules + */ +export const buildJsons: BuildTask = async (db) => { + const isBundleModifed = (bundle: string) => { + const timestamp = db.get(`jsons.${bundle}`) + .value() || 0; + return isFolderModified(`src/bundles/${bundle}`, timestamp); + }; + + const bundleNames = Object.keys(modules); + const filteredBundles = shouldBuildAll('jsons') + ? bundleNames + : bundleNames.filter(isBundleModifed); + + if (filteredBundles.length === 0) { + console.log('Documentation up to date'); + return; + } + + console.log( + chalk.greenBright('Building documentation for the following bundles:'), + ); + console.log( + filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`) + .join('\n'), + ); + + try { + await fs.access('build/jsons', fsConstants.F_OK); + } catch (_error) { + await fs.mkdir('build/jsons'); + } + + const buildTime = new Date() + .getTime(); + + const docsFile = await fs.readFile('build/docs.json', 'utf-8'); + const parsedJSON = JSON.parse(docsFile).children; + + if (!parsedJSON) { + throw new Error('Failed to parse docs.json'); + } + + const bundles = Object.keys(modules) + .map((bundle) => { + const moduleDocs = parsedJSON.find((x) => x.name === bundle); + + if (!moduleDocs || !moduleDocs.children) { + console.warn( + `${chalk.yellow('Warning:')} No documentation found for ${bundle}`, + ); + return [bundle, undefined]; + } + + return [bundle, moduleDocs.children]; + }); + + await Promise.all( + bundles.map(async ([bundle, docs]) => { + if (!docs) return; + + const output = {}; + docs.forEach((element) => { + if (parsers[element.kindString]) { + output[element.name] = parsers[element.kindString](element, bundle); + } else { + console.warn( + `${chalk.yellow('Warning:')} ${bundle}: No parser found for ${ + element.name + } of type ${element.type}`, + ); + } + }); + + await fs.writeFile( + `build/jsons/${bundle}.json`, + JSON.stringify(output, null, 2), + ); + + db.set(`jsons.${bundle}`, buildTime) + .write(); + }), + ); +}; + +/** + * Build the HTML documentation for all modules.\ + * \ + * TypeDoc always clears the directory after each build, so if you leave some modules out + * their documentation won't be properly included. Hence all modules have to be built at + * the same time. + */ +export const buildDocs: BuildTask = async (db) => { + const app = new typedoc.Application(); + app.options.addReader(new typedoc.TSConfigReader()); + app.options.addReader(new typedoc.TypeDocReader()); + + app.bootstrap({ + entryPoints: Object.keys(modules) + .map( + (bundle) => `src/bundles/${bundle}/functions.ts`, + ), + tsconfig: 'src/tsconfig.json', + theme: 'typedoc-modules-theme', + excludeInternal: true, + categorizeByGroup: true, + name: 'Source Academy Modules', + }); + + const project = app.convert(); + if (project) { + const docsTask = app.generateDocs(project, 'build/documentation'); + const jsonTask = app.generateJson(project, 'build/docs.json'); + await Promise.all([docsTask, jsonTask]); + + // For some reason typedoc's not working, so do a manual copy + await fs.copyFile( + `${cjsDirname(import.meta.url)}/README.md`, + 'build/documentation/README.md', + ); + + db.set('docs', new Date() + .getTime()) + .write(); + } +}; + +/** + * Build both JSONS and HTML documentation + */ +export default async () => { + const db = await getDb(); + await buildDocs(db); + await buildJsons(db); +}; diff --git a/scripts/build/index.ts b/scripts/build/index.ts new file mode 100644 index 000000000..8e4826dea --- /dev/null +++ b/scripts/build/index.ts @@ -0,0 +1,16 @@ +import copy from './misc'; +import { buildBundles } from './bundles'; +import { buildTabs } from './tabs'; +import { buildDocs, buildJsons } from './docs'; +import { getDb } from './buildUtils'; + +export default async () => { + const db = await getDb(); + await Promise.all([ + buildBundles(db), + buildTabs(db), + buildDocs(db) + .then(() => buildJsons(db)), + ]); + await copy(); +}; diff --git a/scripts/build/misc.ts b/scripts/build/misc.ts new file mode 100644 index 000000000..83f2ac6f0 --- /dev/null +++ b/scripts/build/misc.ts @@ -0,0 +1,14 @@ +import { promises as fs } from 'fs'; +import { getDbPath } from './buildUtils'; + +/** + * Copy `modules.json` to the build folder + */ +export const copyModules = () => fs.copyFile('modules.json', 'build/modules.json'); + +/** + * Copy `database.json` to the build folder + */ +export const copyDatabase = () => fs.copyFile(getDbPath(), 'build/database.json'); + +export default () => Promise.all([copyModules(), copyDatabase()]); diff --git a/gulpfile.esm.js/build/tabs.js b/scripts/build/tabs.ts similarity index 61% rename from gulpfile.esm.js/build/tabs.js rename to scripts/build/tabs.ts index 6b66e06c1..27f553d0c 100644 --- a/gulpfile.esm.js/build/tabs.js +++ b/scripts/build/tabs.ts @@ -1,84 +1,82 @@ -import gulp from 'gulp'; -import { rollup } from 'rollup'; -import chalk from 'chalk'; -import { promises as fs } from 'fs'; -import { - isFolderModified, - getDb, - removeDuplicates, - defaultConfig, - shouldBuildAll, -} from './utilities'; -import copy from './misc'; -import modules from '../../modules.json'; - -export const convertRawTab = (rawTab) => { - const lastBracket = rawTab.lastIndexOf('('); - return `${rawTab.substring(0, lastBracket)})`; -}; - -/** - * Transpile tabs to the build folder - */ -export const buildTabs = (db) => { - const isTabModifed = (tabName) => { - if (process.argv[3] === '--force') return true; - const timestamp = db.get(`tabs.${tabName}`).value() || 0; - return isFolderModified(`src/tabs/${tabName}`, timestamp); - }; - - const tabNames = removeDuplicates( - Object.values(modules).flatMap((x) => x.tabs) - ); - - const filteredTabs = shouldBuildAll('tabs') - ? tabNames - : tabNames.filter(isTabModifed); - - if (filteredTabs.length === 0) { - return null; - } - - console.log(chalk.greenBright('Building the following tabs:')); - console.log(filteredTabs.map((x) => `• ${chalk.blue(x)}`).join('\n')); - - const buildTime = new Date().getTime(); - - const processTab = async (tabName) => { - const result = await rollup({ - ...defaultConfig, - input: `src/tabs/${tabName}/index.tsx`, - external: ['react', 'react-dom'], - }); - - const tabFile = `build/tabs/${tabName}.js`; - await result.write({ - file: tabFile, - format: 'iife', - globals: { - react: 'React', - 'react-dom': 'ReactDom', - }, - }); - - const rawTab = await fs.readFile(tabFile, 'utf-8'); - await fs.writeFile(tabFile, convertRawTab(rawTab)); - - db.set(`tabs.${tabName}`, buildTime).write(); - }; - - return Promise.all(filteredTabs.map(processTab)); -}; - -export default gulp.series( - Object.assign( - async () => { - const db = await getDb(); - await buildTabs(db); - }, - { - displayName: 'buildTabs', - } - ), - copy -); +import { rollup } from 'rollup'; +import chalk from 'chalk'; +import { promises as fs } from 'fs'; +import { + isFolderModified, + getDb, + removeDuplicates, + defaultConfig, + shouldBuildAll, + BuildTask, +} from './buildUtils'; +import copy from './misc'; +import { modules } from '../utilities'; + +export const convertRawTab = (rawTab: string) => { + const lastBracket = rawTab.lastIndexOf('('); + return `${rawTab.substring(0, lastBracket)})`; +}; + +/** + * Transpile tabs to the build folder + */ +export const buildTabs: BuildTask = (db) => { + const isTabModifed = (tabName: string) => { + if (process.argv[3] === '--force') return true; + const timestamp = db.get(`tabs.${tabName}`) + .value() || 0; + return isFolderModified(`src/tabs/${tabName}`, timestamp); + }; + + const tabNames = removeDuplicates( + Object.values(modules) + .flatMap((x) => x.tabs), + ); + + const filteredTabs = shouldBuildAll('tabs') + ? tabNames + : tabNames.filter(isTabModifed); + + if (filteredTabs.length === 0) { + return null; + } + + console.log(chalk.greenBright('Building the following tabs:')); + console.log(filteredTabs.map((x) => `• ${chalk.blue(x)}`) + .join('\n')); + + const buildTime = new Date() + .getTime(); + + const processTab = async (tabName: string) => { + const result = await rollup({ + ...defaultConfig, + input: `src/tabs/${tabName}/index.tsx`, + external: ['react', 'react-dom'], + }); + + const tabFile = `build/tabs/${tabName}.js`; + await result.write({ + file: tabFile, + format: 'iife', + globals: { + 'react': 'React', + 'react-dom': 'ReactDom', + }, + }); + + const rawTab = await fs.readFile(tabFile, 'utf-8'); + await fs.writeFile(tabFile, convertRawTab(rawTab)); + + db.set(`tabs.${tabName}`, buildTime) + .write(); + }; + + return Promise.all(filteredTabs.map(processTab)); +}; + +export default async () => { + const db = await getDb(); + await buildTabs(db); + await copy(); +}; diff --git a/scripts/index.ts b/scripts/index.ts new file mode 100644 index 000000000..5150da9a2 --- /dev/null +++ b/scripts/index.ts @@ -0,0 +1,19 @@ +import build from './build'; +import buildBundles from './build/bundles'; +import buildTabs from './build/tabs'; +import buildDocs from './build/docs'; +import create from './templates'; + +const tasks = { + build, + 'build:bundles': buildBundles, + 'build:tabs': buildTabs, + 'build:docs': buildDocs, + create, +}; + +function main() { + return tasks[process.argv[2]](); +} + +main(); diff --git a/gulpfile.esm.js/templates/index.js b/scripts/templates/index.ts similarity index 90% rename from gulpfile.esm.js/templates/index.js rename to scripts/templates/index.ts index a0e539349..e693b3ab3 100644 --- a/gulpfile.esm.js/templates/index.js +++ b/scripts/templates/index.ts @@ -1,30 +1,30 @@ -import { error as _error, info, rl, askQuestion, warn } from './print'; -import { addNew as addNewModule } from './module'; -import { addNew as addNewTab } from './tab'; - -async function askMode() { - while (true) { - // eslint-disable-next-line no-await-in-loop - const mode = await askQuestion( - 'What would you like to create? (module/tab)' - ); - if (mode !== 'module' && mode !== 'tab') { - warn("Please answer with only 'module' or 'tab'."); - } else { - return mode; - } - } -} - -export default async function () { - try { - const mode = await askMode(); - if (mode === 'module') await addNewModule(); - else if (mode === 'tab') await addNewTab(); - } catch (error) { - _error(`ERROR: ${error.message}`); - info('Terminating module app...'); - } finally { - rl.close(); - } -} +import { error as _error, info, rl, askQuestion, warn } from './print'; +import { addNew as addNewModule } from './module'; +import { addNew as addNewTab } from './tab'; + +async function askMode() { + while (true) { + // eslint-disable-next-line no-await-in-loop + const mode = await askQuestion( + 'What would you like to create? (module/tab)', + ); + if (mode !== 'module' && mode !== 'tab') { + warn("Please answer with only 'module' or 'tab'."); + } else { + return mode; + } + } +} + +export default async function () { + try { + const mode = await askMode(); + if (mode === 'module') await addNewModule(); + else if (mode === 'tab') await addNewTab(); + } catch (error) { + _error(`ERROR: ${error.message}`); + info('Terminating module app...'); + } finally { + rl.close(); + } +} diff --git a/gulpfile.esm.js/templates/module.js b/scripts/templates/module.ts similarity index 74% rename from gulpfile.esm.js/templates/module.js rename to scripts/templates/module.ts index 79e08d2d4..c73c92076 100644 --- a/gulpfile.esm.js/templates/module.js +++ b/scripts/templates/module.ts @@ -1,40 +1,44 @@ -import { promises as fs } from 'fs'; -import { askQuestion, warn, success } from './print'; -import { isSnakeCase } from './utilities'; -import manifest from '../../modules.json'; -import { cjsDirname } from '../utilities'; - -export function check(moduleName) { - return Object.keys(manifest).includes(moduleName); -} - -async function askModuleName() { - while (true) { - // eslint-disable-next-line no-await-in-loop - const name = await askQuestion( - 'What is the name of your new module? (eg. binary_tree)' - ); - if (isSnakeCase(name) === false) { - warn('Module names must be in snake case. (eg. binary_tree)'); - } else if (check(name)) { - warn('A module with the same name already exists.'); - } else { - return name; - } - } -} - -export async function addNew() { - const moduleName = await askModuleName(); - const bundleDestination = `src/bundles/${moduleName}`; - await fs.mkdir(bundleDestination, { recursive: true }); - await fs.copyFile( - `${cjsDirname()}/__bundle__.ts`, - `${bundleDestination}/index.ts` - ); - await fs.writeFile( - 'modules.json', - JSON.stringify({ ...manifest, [moduleName]: { tabs: [] } }, null, 2) - ); - success(`Bundle for module ${moduleName} created at ${bundleDestination}.`); -} +import { promises as fs } from 'fs'; +import { askQuestion, warn, success } from './print'; +import { isSnakeCase } from './utilities'; +import manifest from '../../modules.json'; +import { cjsDirname } from '../utilities'; + +export function check(moduleName) { + return Object.keys(manifest) + .includes(moduleName); +} + +async function askModuleName() { + while (true) { + // eslint-disable-next-line no-await-in-loop + const name = await askQuestion( + 'What is the name of your new module? (eg. binary_tree)', + ); + if (isSnakeCase(name) === false) { + warn('Module names must be in snake case. (eg. binary_tree)'); + } else if (check(name)) { + warn('A module with the same name already exists.'); + } else { + return name; + } + } +} + +export async function addNew() { + const moduleName = await askModuleName(); + const bundleDestination = `src/bundles/${moduleName}`; + await fs.mkdir(bundleDestination, { recursive: true }); + await fs.copyFile( + `${cjsDirname(import.meta.url)}/__bundle__.ts`, + `${bundleDestination}/index.ts`, + ); + await fs.writeFile( + 'modules.json', + JSON.stringify({ + ...manifest, + [moduleName]: { tabs: [] }, + }, null, 2), + ); + success(`Bundle for module ${moduleName} created at ${bundleDestination}.`); +} diff --git a/scripts/templates/print.ts b/scripts/templates/print.ts new file mode 100644 index 000000000..dea2e93a9 --- /dev/null +++ b/scripts/templates/print.ts @@ -0,0 +1,29 @@ +import chalk from 'chalk'; +import { createInterface } from 'readline'; + +export const rl = createInterface({ + input: process.stdin, + output: process.stdout, +}); + +export function info(...args) { + return console.log(...args.map((string) => chalk.grey(string))); +} + +export function error(...args) { + return console.log(...args.map((string) => chalk.red(string))); +} + +export function warn(...args) { + return console.log(...args.map((string) => chalk.yellow(string))); +} + +export function success(...args) { + return console.log(...args.map((string) => chalk.green(string))); +} + +export function askQuestion(question) { + return new Promise((resolve) => { + rl.question(chalk.blueBright(`${question}\n`), resolve); + }); +} diff --git a/gulpfile.esm.js/templates/tab.js b/scripts/templates/tab.ts similarity index 82% rename from gulpfile.esm.js/templates/tab.js rename to scripts/templates/tab.ts index ed94c2501..d4bde0029 100644 --- a/gulpfile.esm.js/templates/tab.js +++ b/scripts/templates/tab.ts @@ -1,66 +1,67 @@ -/* eslint-disable no-await-in-loop */ -import { promises as fs } from 'fs'; -import { askQuestion, warn, success } from './print'; -import { isPascalCase } from './utilities'; -import { check as _check } from './module'; -import manifest from '../../modules.json'; -import { cjsDirname } from '../utilities'; - -const existingTabs = Object.values(manifest).flatMap((value) => value.tabs); - -export function check(tabName) { - return existingTabs.includes(tabName); -} - -async function askModuleName() { - while (true) { - const name = await askQuestion('Add a new tab to which module?'); - if (!_check(name)) { - warn(`Module ${name} does not exist.`); - } else { - return name; - } - } -} - -async function askTabName() { - while (true) { - const name = await askQuestion( - 'What is the name of your new tab? (eg. BinaryTree)' - ); - if (!isPascalCase(name)) { - warn('Tab names must be in pascal case. (eg. BinaryTree)'); - } else if (check(name)) { - warn('A tab with the same name already exists.'); - } else { - return name; - } - } -} - -export async function addNew() { - const moduleName = await askModuleName(); - const tabName = await askTabName(); - - // Copy module tab template into correct destination and show success message - const tabDestination = `src/tabs/${tabName}`; - await fs.mkdir(tabDestination, { recursive: true }); - await fs.copyFile( - `${cjsDirname()}/templates/__templates__.ts`, - `${tabDestination}/index.tsx` - ); - await fs.writeFile( - 'modules.json', - JSON.stringify( - { - ...manifest, - [moduleName]: { tabs: [...manifest[moduleName].tabs, tabName] }, - }, - null, - 2 - ) - ); - success( - `Tab ${tabName} for module ${moduleName} created at ${tabDestination}.` - ); -} +/* eslint-disable no-await-in-loop */ +import { promises as fs } from 'fs'; +import { askQuestion, warn, success } from './print'; +import { isPascalCase } from './utilities'; +import { check as _check } from './module'; +import manifest from '../../modules.json'; +import { cjsDirname } from '../utilities'; + +const existingTabs = Object.values(manifest) + .flatMap((value) => value.tabs); + +export function check(tabName) { + return existingTabs.includes(tabName); +} + +async function askModuleName() { + while (true) { + const name = await askQuestion('Add a new tab to which module?'); + if (!_check(name)) { + warn(`Module ${name} does not exist.`); + } else { + return name; + } + } +} + +async function askTabName() { + while (true) { + const name = await askQuestion( + 'What is the name of your new tab? (eg. BinaryTree)', + ); + if (!isPascalCase(name)) { + warn('Tab names must be in pascal case. (eg. BinaryTree)'); + } else if (check(name)) { + warn('A tab with the same name already exists.'); + } else { + return name; + } + } +} + +export async function addNew() { + const moduleName = await askModuleName(); + const tabName = await askTabName(); + + // Copy module tab template into correct destination and show success message + const tabDestination = `src/tabs/${tabName}`; + await fs.mkdir(tabDestination, { recursive: true }); + await fs.copyFile( + `${cjsDirname(import.meta.url)}/templates/__templates__.ts`, + `${tabDestination}/index.tsx`, + ); + await fs.writeFile( + 'modules.json', + JSON.stringify( + { + ...manifest, + [moduleName]: { tabs: [...manifest[moduleName].tabs, tabName] }, + }, + null, + 2, + ), + ); + success( + `Tab ${tabName} for module ${moduleName} created at ${tabDestination}.`, + ); +} diff --git a/gulpfile.esm.js/templates/templates/__bundle__.ts b/scripts/templates/templates/__bundle__.ts similarity index 100% rename from gulpfile.esm.js/templates/templates/__bundle__.ts rename to scripts/templates/templates/__bundle__.ts diff --git a/gulpfile.esm.js/templates/templates/__tab__.tsx b/scripts/templates/templates/__tab__.tsx similarity index 100% rename from gulpfile.esm.js/templates/templates/__tab__.tsx rename to scripts/templates/templates/__tab__.tsx diff --git a/gulpfile.esm.js/templates/utilities.js b/scripts/templates/utilities.ts similarity index 96% rename from gulpfile.esm.js/templates/utilities.js rename to scripts/templates/utilities.ts index f5289fd6b..42db35cbc 100644 --- a/gulpfile.esm.js/templates/utilities.js +++ b/scripts/templates/utilities.ts @@ -1,10 +1,10 @@ -const snakeCaseRegex = /\b[a-z]+(?:_[a-z]+)*\b/u; -const pascalCaseRegex = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u; - -export function isSnakeCase(string) { - return snakeCaseRegex.test(string); -} - -export function isPascalCase(string) { - return pascalCaseRegex.test(string); -} +const snakeCaseRegex = /\b[a-z]+(?:_[a-z]+)*\b/u; +const pascalCaseRegex = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u; + +export function isSnakeCase(string) { + return snakeCaseRegex.test(string); +} + +export function isPascalCase(string) { + return pascalCaseRegex.test(string); +} diff --git a/scripts/utilities.ts b/scripts/utilities.ts new file mode 100644 index 000000000..d9e5f3d04 --- /dev/null +++ b/scripts/utilities.ts @@ -0,0 +1,19 @@ +/** + * Utilities for scripts + */ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import _modules from '../modules.json'; + +export type ModuleManifest = { + [name: string]: { + tabs: string[]; + }; +}; + +export const modules = _modules as ModuleManifest; + +/** + * Function to replace the functionality of `__dirname` in CommonJS modules + */ +export const cjsDirname = (url: string) => dirname(fileURLToPath(url)); diff --git a/src/bundles/binary_tree/functions.ts b/src/bundles/binary_tree/functions.ts index 1120fec6b..29ebe4fec 100644 --- a/src/bundles/binary_tree/functions.ts +++ b/src/bundles/binary_tree/functions.ts @@ -35,7 +35,7 @@ export function make_empty_tree(): BinaryTree { export function make_tree( value: any, left: BinaryTree, - right: BinaryTree + right: BinaryTree, ): BinaryTree { return [left, value, right]; } From d9868797db780c7172272e0d79e83cd69f8f040a Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Fri, 12 Aug 2022 14:17:08 +0800 Subject: [PATCH 22/51] Bumped lowdb version --- .prettierrc.json | 7 ----- package.json | 18 +++++------- scripts/build/buildUtils.ts | 41 ++++++++++++++++++-------- scripts/build/bundles.ts | 7 ++--- scripts/build/docs/index.ts | 28 +++++++++--------- scripts/build/tabs.ts | 7 ++--- scripts/index.ts | 21 ++++++++++++-- scripts/templates/module.ts | 5 ++-- scripts/templates/tab.ts | 5 ++-- scripts/templates/utilities.ts | 4 +-- yarn.lock | 53 +++++++--------------------------- 11 files changed, 91 insertions(+), 105 deletions(-) delete mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 57670c03a..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "es5", - "jsxSingleQuote": true, - "printWidth": 80, - "endOfLine": "auto" -} diff --git a/package.json b/package.json index 2b7a0041f..51a7f0090 100644 --- a/package.json +++ b/package.json @@ -14,22 +14,23 @@ "build:tabs": "Transpile tabs into the `build/` folder and copy over `modules.json`", "build:docs": "Generate TypeDocs in the `build/documentation/` folder", "serve": "Start the HTTP server to serve all files in `build/`, with the same directory structure", + "scripts": "Run a script within the script server using ts-node", "dev": "Rebuild modules, then serve", "prepare": "Enable git hooks", "test": "Run unit tests", "test:watch": "Watch files for changes and rerun tests related to changed files" }, "scripts": { - "gulp": "cross-env TS_NODE_PROJECT=gulpfile.ts/tsconfig.json gulp", - "create": "gulp create", + "create": "yarn scripts create", "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", "build": "yarn lint && gulp build", - "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildBundles", - "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && gulp buildTabs", - "build:docs": "gulp buildDocs", + "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:bundles", + "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:tabs", + "build:docs": "yarn scripts build:docs", "serve": "http-server --cors=* -c-1 -p 8022 ./build", "dev": "yarn build && yarn serve", + "scripts": "cross-env TS_NODE_PROJECT=scripts/tsconfig.json ts-node scripts/index.ts", "prepare": "husky install", "test": "jest --verbose", "test:watch": "jest --watch" @@ -47,7 +48,6 @@ "@types/gulp": "^4.0.9", "@types/jest": "^27.4.1", "@types/lowdb": "^1.0.11", - "@types/merge-stream": "^1.1.2", "@types/node": "^17.0.23", "@types/react": "^17.0.43", "@typescript-eslint/eslint-plugin": "^5.18.0", @@ -66,13 +66,11 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.4.0", - "esm": "^3.2.25", "generate-template-files": "^3.0.0", "http-server": "^0.12.3", "husky": "5", "jest": "^26.6.3", - "lowdb": "^1.0.0", - "merge-stream": "^2.0.0", + "lowdb": "^3.0.0", "rollup": "^2.41.2", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-commonjs": "^10.1.0", @@ -107,7 +105,7 @@ "roots": [ "/src/bundles", "/src/tabs", - "/gulpfile.esm.js" + "/scripts" ] } } diff --git a/scripts/build/buildUtils.ts b/scripts/build/buildUtils.ts index 835c9d595..0eaaee7b2 100644 --- a/scripts/build/buildUtils.ts +++ b/scripts/build/buildUtils.ts @@ -8,8 +8,7 @@ import commonJS from 'rollup-plugin-commonjs'; import filesize from 'rollup-plugin-filesize'; import injectProcessEnv from 'rollup-plugin-inject-process-env'; import { join } from 'path'; -import Low from 'lowdb'; -import FileAsync from 'lowdb/adapters/FileAsync'; +import { Low, JSONFile } from 'lowdb'; import { DATABASE_NAME, NODE_MODULES_PATTERN, @@ -101,25 +100,43 @@ export function getDbPath() { return join(cjsDirname(import.meta.url), `${DATABASE_NAME}.json`); } -type DBKeys = 'jsons' | 'bundles' | 'tabs'; +const DBKeys = ['jsons', 'bundles', 'tabs'] as const; + +type ObjectFromList, V = string> = { + [K in (T extends ReadonlyArray ? U : never)]: V +}; export type DBType = { docs: number; -} & { - [k in DBKeys]: { - [name: string]: number; - } -}; +} & ObjectFromList /** * Get a new Lowdb instance */ -export function getDb() { - // eslint-disable-next-line new-cap - return Low(new FileAsync(getDbPath())); +export async function getDb() { + const db = new Low(new JSONFile(getDbPath())); + await db.read(); + + if (!db.data) { + db.data = { + docs: 0, + jsons: {}, + bundles: {}, + tabs: {} + } + } + // } else { + // for (const element of DBKeys.values()) { + // if (!db.data[element]) db.data[element] = {}; + // }; + // } + + return db; } -export type BuildTask = (db: Low.LowdbAsync) => Promise; +export type BuildTask = (db: Low) => Promise; export function removeDuplicates(arr: T[]) { return [...new Set(arr)]; diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index 2ecc46364..e768222d6 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -16,8 +16,7 @@ import { export const buildBundles: BuildTask = (db) => { const isBundleModifed = (bundle: string) => { if (process.argv[3] === '--force') return true; - const timestamp = db.get(`bundles.${bundle}`) - .value() || 0; + const timestamp = db.data.bundles[bundle] ?? 0; return isFolderModified(`src/bundles/${bundle}`, timestamp); }; @@ -51,8 +50,8 @@ export const buildBundles: BuildTask = (db) => { format: 'iife', }); - db.set(`bundle.${bundle}`, buildTime) - .write(); + db.data.bundles[bundle] = buildTime + await db.write(); }; return Promise.all(filteredBundles.map(processBundle)); diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index 69c0306c0..d965a87a7 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -14,9 +14,11 @@ import { cjsDirname, modules } from '../../utilities'; * Convert each element type (e.g. variable, function) to its respective HTML docstring * to be displayed to users */ -const parsers = { +const parsers: { + [name: string]: (element: any, bundle: string) => string +} = { Variable(element, bundle) { - let desc; + let desc: string; if (element.comment && element.comment.shortText) { desc = drawdown(element.comment.shortText); } else { @@ -44,7 +46,7 @@ const parsers = { const signature = element.signatures[0]; // Form the parameter string for the function - let paramStr; + let paramStr: string; if (!signature.parameters) paramStr = '()'; else { paramStr = `(${signature.parameters @@ -56,11 +58,9 @@ const parsers = { } // Form the result representation for the function - let resultStr; - if (!signature.type) resultStr = 'void'; - else resultStr = signature.type.name; + const resultStr = !signature.type ? 'void' : signature.type.name; - let desc; + let desc: string; if (signature.comment && signature.comment.shortText) { desc = drawdown(signature.comment.shortText); } else { @@ -81,8 +81,7 @@ const parsers = { */ export const buildJsons: BuildTask = async (db) => { const isBundleModifed = (bundle: string) => { - const timestamp = db.get(`jsons.${bundle}`) - .value() || 0; + const timestamp = db.data.jsons[bundle] ?? 0; return isFolderModified(`src/bundles/${bundle}`, timestamp); }; @@ -156,10 +155,11 @@ export const buildJsons: BuildTask = async (db) => { JSON.stringify(output, null, 2), ); - db.set(`jsons.${bundle}`, buildTime) - .write(); + db.data.jsons[bundle] = buildTime }), ); + + await db.write(); }; /** @@ -198,9 +198,9 @@ export const buildDocs: BuildTask = async (db) => { 'build/documentation/README.md', ); - db.set('docs', new Date() - .getTime()) - .write(); + db.data.docs = new Date() + .getTime() + await db.write(); } }; diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts index 27f553d0c..aec8809a0 100644 --- a/scripts/build/tabs.ts +++ b/scripts/build/tabs.ts @@ -23,8 +23,7 @@ export const convertRawTab = (rawTab: string) => { export const buildTabs: BuildTask = (db) => { const isTabModifed = (tabName: string) => { if (process.argv[3] === '--force') return true; - const timestamp = db.get(`tabs.${tabName}`) - .value() || 0; + const timestamp = db.data.tabs[tabName] ?? 0; return isFolderModified(`src/tabs/${tabName}`, timestamp); }; @@ -68,8 +67,8 @@ export const buildTabs: BuildTask = (db) => { const rawTab = await fs.readFile(tabFile, 'utf-8'); await fs.writeFile(tabFile, convertRawTab(rawTab)); - db.set(`tabs.${tabName}`, buildTime) - .write(); + db.data.tabs[tabName] = buildTime + await db.write(); }; return Promise.all(filteredTabs.map(processTab)); diff --git a/scripts/index.ts b/scripts/index.ts index 5150da9a2..4d7f1dbfb 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -3,6 +3,7 @@ import buildBundles from './build/bundles'; import buildTabs from './build/tabs'; import buildDocs from './build/docs'; import create from './templates'; +import chalk from 'chalk'; const tasks = { build, @@ -12,8 +13,22 @@ const tasks = { create, }; -function main() { - return tasks[process.argv[2]](); +async function main() { + if (process.argv.length < 3) { + console.log(chalk.green('Available tasks:')); + console.log(Object.keys(tasks) + .map((each) => `• ${each}`) + .join('\n')); + return; + } + + const task = tasks[process.argv[2]]; + + if (!task) console.error(chalk.redBright(`Unknown task: ${process.argv[2]}`)); + else await task(); } -main(); +main() + .then(() => process.exit(0)); +// I think LowDB is keeping the process alive after it should die +// but I haven't found how to close it so process.exit will have to do diff --git a/scripts/templates/module.ts b/scripts/templates/module.ts index c73c92076..33396f7fc 100644 --- a/scripts/templates/module.ts +++ b/scripts/templates/module.ts @@ -1,10 +1,9 @@ import { promises as fs } from 'fs'; import { askQuestion, warn, success } from './print'; import { isSnakeCase } from './utilities'; -import manifest from '../../modules.json'; -import { cjsDirname } from '../utilities'; +import { modules as manifest, cjsDirname } from '../utilities'; -export function check(moduleName) { +export function check(moduleName: string) { return Object.keys(manifest) .includes(moduleName); } diff --git a/scripts/templates/tab.ts b/scripts/templates/tab.ts index d4bde0029..6ec4b7337 100644 --- a/scripts/templates/tab.ts +++ b/scripts/templates/tab.ts @@ -3,13 +3,12 @@ import { promises as fs } from 'fs'; import { askQuestion, warn, success } from './print'; import { isPascalCase } from './utilities'; import { check as _check } from './module'; -import manifest from '../../modules.json'; -import { cjsDirname } from '../utilities'; +import { modules as manifest, cjsDirname } from '../utilities'; const existingTabs = Object.values(manifest) .flatMap((value) => value.tabs); -export function check(tabName) { +export function check(tabName: string) { return existingTabs.includes(tabName); } diff --git a/scripts/templates/utilities.ts b/scripts/templates/utilities.ts index 42db35cbc..28dc4102e 100644 --- a/scripts/templates/utilities.ts +++ b/scripts/templates/utilities.ts @@ -1,10 +1,10 @@ const snakeCaseRegex = /\b[a-z]+(?:_[a-z]+)*\b/u; const pascalCaseRegex = /^[A-Z][a-z]+(?:[A-Z][a-z]+)*$/u; -export function isSnakeCase(string) { +export function isSnakeCase(string: string) { return snakeCaseRegex.test(string); } -export function isPascalCase(string) { +export function isPascalCase(string: string) { return pascalCaseRegex.test(string); } diff --git a/yarn.lock b/yarn.lock index 22033d93a..78eaa5147 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1791,13 +1791,6 @@ dependencies: "@types/lodash" "*" -"@types/merge-stream@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1" - integrity sha512-7faLmaE99g/yX0Y9pF1neh2IUqOf/fXMOWCVzsXjqI1EJ91lrgXmaBKf6bRWM164lLyiHxHt6t/ZO/cIzq61XA== - dependencies: - "@types/node" "*" - "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -3698,11 +3691,6 @@ eslint@^8.21.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -esm@^3.2.25: - version "3.2.25" - resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - espree@^9.3.2, espree@^9.3.3: version "9.3.3" resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d" @@ -4339,11 +4327,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== -graceful-fs@^4.1.3: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -4940,11 +4923,6 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-promise@^2.1.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== - is-reference@^1.1.2: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" @@ -5774,7 +5752,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4, lodash@4.x, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5786,16 +5764,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lowdb@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" - integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== +lowdb@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-3.0.0.tgz#c10ab4e7eb86f1cbe255e35e60ffb0c6f42049e0" + integrity sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg== dependencies: - graceful-fs "^4.1.3" - is-promise "^2.1.0" - lodash "4" - pify "^3.0.0" - steno "^0.4.1" + steno "^2.1.0" lru-cache@^5.1.1: version "5.1.1" @@ -6690,11 +6664,6 @@ pify@^2.0.0, pify@^2.3.0: resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== - pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -7779,12 +7748,10 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -steno@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" - integrity sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w== - dependencies: - graceful-fs "^4.1.3" +steno@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/steno/-/steno-2.1.0.tgz#05a9c378ce42ed04f642cda6fcb41787a10e4e33" + integrity sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA== stream-exhaust@^1.0.1: version "1.0.2" From 78bf13a1ab8a3edfc9ec962a8e6fd3a03b336029 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 13 Aug 2022 00:17:34 +0800 Subject: [PATCH 23/51] Made build scripts more efficient --- scripts/build/bundles.ts | 6 +++--- scripts/build/tabs.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index e768222d6..83a8aa8fb 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -13,7 +13,7 @@ import { /** * Transpile bundles to the build folder */ -export const buildBundles: BuildTask = (db) => { +export const buildBundles: BuildTask = async (db) => { const isBundleModifed = (bundle: string) => { if (process.argv[3] === '--force') return true; const timestamp = db.data.bundles[bundle] ?? 0; @@ -51,10 +51,10 @@ export const buildBundles: BuildTask = (db) => { }); db.data.bundles[bundle] = buildTime - await db.write(); }; - return Promise.all(filteredBundles.map(processBundle)); + await Promise.all(filteredBundles.map(processBundle)); + await db.write(); }; export default async () => { diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts index aec8809a0..fb34d2243 100644 --- a/scripts/build/tabs.ts +++ b/scripts/build/tabs.ts @@ -20,7 +20,7 @@ export const convertRawTab = (rawTab: string) => { /** * Transpile tabs to the build folder */ -export const buildTabs: BuildTask = (db) => { +export const buildTabs: BuildTask = async (db) => { const isTabModifed = (tabName: string) => { if (process.argv[3] === '--force') return true; const timestamp = db.data.tabs[tabName] ?? 0; @@ -37,7 +37,8 @@ export const buildTabs: BuildTask = (db) => { : tabNames.filter(isTabModifed); if (filteredTabs.length === 0) { - return null; + console.log(chalk.greenBright('All tabs up to date')); + return; } console.log(chalk.greenBright('Building the following tabs:')); @@ -55,7 +56,7 @@ export const buildTabs: BuildTask = (db) => { }); const tabFile = `build/tabs/${tabName}.js`; - await result.write({ + const { output: rollupOutput } = await result.generate({ file: tabFile, format: 'iife', globals: { @@ -64,14 +65,14 @@ export const buildTabs: BuildTask = (db) => { }, }); - const rawTab = await fs.readFile(tabFile, 'utf-8'); + const rawTab = rollupOutput[0].code; await fs.writeFile(tabFile, convertRawTab(rawTab)); db.data.tabs[tabName] = buildTime - await db.write(); }; - return Promise.all(filteredTabs.map(processTab)); + await Promise.all(filteredTabs.map(processTab)); + await db.write(); }; export default async () => { From 77704ab6136fb85d8bdc22da0c73617f1b6ba719 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Sat, 13 Aug 2022 23:38:11 +0800 Subject: [PATCH 24/51] Added result.close calls In accordance with rollup best practices, call the close function after rollup build is completed --- package.json | 2 +- scripts/build/buildUtils.ts | 7 +------ scripts/build/bundles.ts | 1 + scripts/build/tabs.ts | 4 +++- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 51a7f0090..c777eedce 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "create": "yarn scripts create", "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", - "build": "yarn lint && gulp build", + "build": "yarn lint && yarn scripts build", "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:bundles", "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:tabs", "build:docs": "yarn scripts build:docs", diff --git a/scripts/build/buildUtils.ts b/scripts/build/buildUtils.ts index 0eaaee7b2..8229d2a48 100644 --- a/scripts/build/buildUtils.ts +++ b/scripts/build/buildUtils.ts @@ -120,6 +120,7 @@ export async function getDb() { await db.read(); if (!db.data) { + // Set default data if database.json is missing db.data = { docs: 0, jsons: {}, @@ -127,12 +128,6 @@ export async function getDb() { tabs: {} } } - // } else { - // for (const element of DBKeys.values()) { - // if (!db.data[element]) db.data[element] = {}; - // }; - // } - return db; } diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index 83a8aa8fb..beae69af5 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -49,6 +49,7 @@ export const buildBundles: BuildTask = async (db) => { file: `build/bundles/${bundle}.js`, format: 'iife', }); + await result.close(); db.data.bundles[bundle] = buildTime }; diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts index fb34d2243..a455baf19 100644 --- a/scripts/build/tabs.ts +++ b/scripts/build/tabs.ts @@ -64,9 +64,11 @@ export const buildTabs: BuildTask = async (db) => { 'react-dom': 'ReactDom', }, }); - + + // Only one chunk should be generated const rawTab = rollupOutput[0].code; await fs.writeFile(tabFile, convertRawTab(rawTab)); + await result.close(); db.data.tabs[tabName] = buildTime }; From 743c79f7983341f4ad97b45afd10fd5ff097e114 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 00:01:37 +0800 Subject: [PATCH 25/51] Readded constants --- scripts/build/bundles.ts | 5 +++-- scripts/build/docs/index.ts | 21 ++++++++++++--------- scripts/templates/module.ts | 4 ++-- scripts/templates/print.ts | 2 +- src/tabs/Rune/index.tsx | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index beae69af5..73ac6ec16 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -9,6 +9,7 @@ import { isFolderModified, shouldBuildAll, } from './buildUtils'; +import { BUILD_PATH, SOURCE_PATH } from './constants'; /** * Transpile bundles to the build folder @@ -42,11 +43,11 @@ export const buildBundles: BuildTask = async (db) => { const processBundle = async (bundle: string) => { const result = await rollup({ ...defaultConfig, - input: `src/bundles/${bundle}/index.ts`, + input: `${SOURCE_PATH}/bundles/${bundle}/index.ts`, }); await result.write({ - file: `build/bundles/${bundle}.js`, + file: `${BUILD_PATH}/bundles/${bundle}.js`, format: 'iife', }); await result.close(); diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index d965a87a7..e011250b9 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -8,7 +8,8 @@ import { shouldBuildAll, BuildTask, } from '../buildUtils'; -import { cjsDirname, modules } from '../../utilities'; +import { cjsDirname, modules as manifest } from '../../utilities'; +import { BUILD_PATH, SOURCE_PATH } from '../constants'; /** * Convert each element type (e.g. variable, function) to its respective HTML docstring @@ -85,7 +86,7 @@ export const buildJsons: BuildTask = async (db) => { return isFolderModified(`src/bundles/${bundle}`, timestamp); }; - const bundleNames = Object.keys(modules); + const bundleNames = Object.keys(manifest); const filteredBundles = shouldBuildAll('jsons') ? bundleNames : bundleNames.filter(isBundleModifed); @@ -119,7 +120,7 @@ export const buildJsons: BuildTask = async (db) => { throw new Error('Failed to parse docs.json'); } - const bundles = Object.keys(modules) + const bundles: [string, any][] = Object.keys(manifest) .map((bundle) => { const moduleDocs = parsedJSON.find((x) => x.name === bundle); @@ -137,8 +138,9 @@ export const buildJsons: BuildTask = async (db) => { bundles.map(async ([bundle, docs]) => { if (!docs) return; - const output = {}; - docs.forEach((element) => { + // Run through each item in the bundle and run its parser + const output: { [name: string]: string } = {}; + docs.forEach((element: any) => { if (parsers[element.kindString]) { output[element.name] = parsers[element.kindString](element, bundle); } else { @@ -150,8 +152,9 @@ export const buildJsons: BuildTask = async (db) => { } }); + // Then write that output to the bundles' respective json files await fs.writeFile( - `build/jsons/${bundle}.json`, + `${BUILD_PATH}/jsons/${bundle}.json`, JSON.stringify(output, null, 2), ); @@ -175,9 +178,9 @@ export const buildDocs: BuildTask = async (db) => { app.options.addReader(new typedoc.TypeDocReader()); app.bootstrap({ - entryPoints: Object.keys(modules) + entryPoints: Object.keys(manifest) .map( - (bundle) => `src/bundles/${bundle}/functions.ts`, + (bundle) => `${SOURCE_PATH}/bundles/${bundle}/functions.ts`, ), tsconfig: 'src/tsconfig.json', theme: 'typedoc-modules-theme', @@ -195,7 +198,7 @@ export const buildDocs: BuildTask = async (db) => { // For some reason typedoc's not working, so do a manual copy await fs.copyFile( `${cjsDirname(import.meta.url)}/README.md`, - 'build/documentation/README.md', + `${BUILD_PATH}/documentation/README.md`, ); db.data.docs = new Date() diff --git a/scripts/templates/module.ts b/scripts/templates/module.ts index 33396f7fc..8fbceb292 100644 --- a/scripts/templates/module.ts +++ b/scripts/templates/module.ts @@ -1,7 +1,7 @@ +import { askQuestion, success, warn } from './print'; +import { cjsDirname, modules as manifest } from '../utilities'; import { promises as fs } from 'fs'; -import { askQuestion, warn, success } from './print'; import { isSnakeCase } from './utilities'; -import { modules as manifest, cjsDirname } from '../utilities'; export function check(moduleName: string) { return Object.keys(manifest) diff --git a/scripts/templates/print.ts b/scripts/templates/print.ts index dea2e93a9..9433dd499 100644 --- a/scripts/templates/print.ts +++ b/scripts/templates/print.ts @@ -22,7 +22,7 @@ export function success(...args) { return console.log(...args.map((string) => chalk.green(string))); } -export function askQuestion(question) { +export function askQuestion(question: string) { return new Promise((resolve) => { rl.question(chalk.blueBright(`${question}\n`), resolve); }); diff --git a/src/tabs/Rune/index.tsx b/src/tabs/Rune/index.tsx index 1cdc32003..a5b521fae 100644 --- a/src/tabs/Rune/index.tsx +++ b/src/tabs/Rune/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { HollusionRune } from '../../bundles/rune/functions'; -import { +import type { AnimatedRune, DrawnRune, RunesModuleState, From aa9cd5fc58dbe4da3da0898a12d224cd20768366 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 00:01:46 +0800 Subject: [PATCH 26/51] Trying to fix eslint --- .eslintignore | 3 ++- .eslintrc.base.js | 11 ++++++++++- src/.eslintrc.js | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index ea1058073..31f010ad3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ .husky/ build/ -**/.eslintrc*.js \ No newline at end of file +**/.eslintrc*.js +scripts/templates/templates \ No newline at end of file diff --git a/.eslintrc.base.js b/.eslintrc.base.js index e65f2b28c..deba694a2 100644 --- a/.eslintrc.base.js +++ b/.eslintrc.base.js @@ -318,7 +318,6 @@ module.exports = { 'require-await': 1, 'require-unicode-regexp': 1, 'require-yield': 1, // Was 2 - // 'sort-imports': 0, // "sort-keys": 0, // "sort-vars": 0, 'spaced-comment': [ @@ -493,4 +492,14 @@ module.exports = { // "wrap-regex": 0, 'yield-star-spacing': 1, }, + overrides: [ + { + files: ['src/**/__tests__/**.ts', 'scripts/**/__tests__/**.ts'], + extends: ['airbnb-typescript', 'plugin:jest/recommended'], + plugins: ['jest'], + rules: { + 'no-console': 0, + } + } + ] }; diff --git a/src/.eslintrc.js b/src/.eslintrc.js index ab7770364..96ab5bf1d 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -92,6 +92,7 @@ module.exports = { } ], "@typescript-eslint/lines-between-class-members": 0, // Was 2 + // "@typescript-eslint/consistent-type-imports": 1, // [Error → Warn] /* NOTE From cb4b9fc13bde28d38b9c670f11b9fef28b6a5711 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 01:00:57 +0800 Subject: [PATCH 27/51] Updated templates to use constants --- scripts/build/buildUtils.ts | 4 ++-- scripts/build/bundles.ts | 2 +- scripts/build/docs/index.ts | 2 +- scripts/{build => }/constants.ts | 3 +-- scripts/templates/module.ts | 3 ++- scripts/templates/tab.ts | 3 ++- 6 files changed, 9 insertions(+), 8 deletions(-) rename scripts/{build => }/constants.ts (77%) diff --git a/scripts/build/buildUtils.ts b/scripts/build/buildUtils.ts index 8229d2a48..a9c948941 100644 --- a/scripts/build/buildUtils.ts +++ b/scripts/build/buildUtils.ts @@ -14,7 +14,7 @@ import { NODE_MODULES_PATTERN, SOURCE_PATTERN, SUPPRESSED_WARNINGS, -} from './constants'; +} from '../constants'; import { cjsDirname } from '../utilities'; /** @@ -97,7 +97,7 @@ export function isFolderModified(relativeFolderPath: string, storedTimestamp: nu * Get the path to the database file */ export function getDbPath() { - return join(cjsDirname(import.meta.url), `${DATABASE_NAME}.json`); + return join(cjsDirname(import.meta.url), DATABASE_NAME); } const DBKeys = ['jsons', 'bundles', 'tabs'] as const; diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index 73ac6ec16..ebb3c4554 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -9,7 +9,7 @@ import { isFolderModified, shouldBuildAll, } from './buildUtils'; -import { BUILD_PATH, SOURCE_PATH } from './constants'; +import { BUILD_PATH, SOURCE_PATH } from '../constants'; /** * Transpile bundles to the build folder diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index e011250b9..95a07c3d1 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -9,7 +9,7 @@ import { BuildTask, } from '../buildUtils'; import { cjsDirname, modules as manifest } from '../../utilities'; -import { BUILD_PATH, SOURCE_PATH } from '../constants'; +import { BUILD_PATH, SOURCE_PATH } from '../../constants'; /** * Convert each element type (e.g. variable, function) to its respective HTML docstring diff --git a/scripts/build/constants.ts b/scripts/constants.ts similarity index 77% rename from scripts/build/constants.ts rename to scripts/constants.ts index edc898a77..3bf3cce06 100644 --- a/scripts/build/constants.ts +++ b/scripts/constants.ts @@ -1,8 +1,7 @@ /* [Exports] */ export const SUPPRESSED_WARNINGS = ['MISSING_NAME_OPTION_FOR_IIFE_EXPORT']; -export const DATABASE_NAME = 'database'; -export const DATABASE_KEY = 'timestamp'; +export const DATABASE_NAME = 'database.json'; export const SOURCE_PATH = './src/'; export const BUILD_PATH = './build/'; diff --git a/scripts/templates/module.ts b/scripts/templates/module.ts index 8fbceb292..2378f59f6 100644 --- a/scripts/templates/module.ts +++ b/scripts/templates/module.ts @@ -2,6 +2,7 @@ import { askQuestion, success, warn } from './print'; import { cjsDirname, modules as manifest } from '../utilities'; import { promises as fs } from 'fs'; import { isSnakeCase } from './utilities'; +import { SOURCE_PATH } from '../constants'; export function check(moduleName: string) { return Object.keys(manifest) @@ -26,7 +27,7 @@ async function askModuleName() { export async function addNew() { const moduleName = await askModuleName(); - const bundleDestination = `src/bundles/${moduleName}`; + const bundleDestination = `${SOURCE_PATH}/bundles/${moduleName}`; await fs.mkdir(bundleDestination, { recursive: true }); await fs.copyFile( `${cjsDirname(import.meta.url)}/__bundle__.ts`, diff --git a/scripts/templates/tab.ts b/scripts/templates/tab.ts index 6ec4b7337..4fab87150 100644 --- a/scripts/templates/tab.ts +++ b/scripts/templates/tab.ts @@ -4,6 +4,7 @@ import { askQuestion, warn, success } from './print'; import { isPascalCase } from './utilities'; import { check as _check } from './module'; import { modules as manifest, cjsDirname } from '../utilities'; +import { SOURCE_PATH } from '../constants'; const existingTabs = Object.values(manifest) .flatMap((value) => value.tabs); @@ -43,7 +44,7 @@ export async function addNew() { const tabName = await askTabName(); // Copy module tab template into correct destination and show success message - const tabDestination = `src/tabs/${tabName}`; + const tabDestination = `${SOURCE_PATH}/tabs/${tabName}`; await fs.mkdir(tabDestination, { recursive: true }); await fs.copyFile( `${cjsDirname(import.meta.url)}/templates/__templates__.ts`, From ca79cb526fe14a79e148d83fb3c5d8b26e2b0238 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 14:56:31 +0800 Subject: [PATCH 28/51] Continued to move toward constants --- scripts/build/bundles.ts | 2 +- scripts/build/docs/index.ts | 6 +++--- scripts/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts index ebb3c4554..bf1f7c3bc 100644 --- a/scripts/build/bundles.ts +++ b/scripts/build/bundles.ts @@ -18,7 +18,7 @@ export const buildBundles: BuildTask = async (db) => { const isBundleModifed = (bundle: string) => { if (process.argv[3] === '--force') return true; const timestamp = db.data.bundles[bundle] ?? 0; - return isFolderModified(`src/bundles/${bundle}`, timestamp); + return isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, timestamp); }; const bundleNames = Object.keys(modules); diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index 95a07c3d1..2e51c4c52 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -83,7 +83,7 @@ const parsers: { export const buildJsons: BuildTask = async (db) => { const isBundleModifed = (bundle: string) => { const timestamp = db.data.jsons[bundle] ?? 0; - return isFolderModified(`src/bundles/${bundle}`, timestamp); + return isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, timestamp); }; const bundleNames = Object.keys(manifest); @@ -120,7 +120,7 @@ export const buildJsons: BuildTask = async (db) => { throw new Error('Failed to parse docs.json'); } - const bundles: [string, any][] = Object.keys(manifest) + const bundles: [string, undefined | any[]][] = Object.keys(manifest) .map((bundle) => { const moduleDocs = parsedJSON.find((x) => x.name === bundle); @@ -140,7 +140,7 @@ export const buildJsons: BuildTask = async (db) => { // Run through each item in the bundle and run its parser const output: { [name: string]: string } = {}; - docs.forEach((element: any) => { + docs.forEach((element) => { if (parsers[element.kindString]) { output[element.name] = parsers[element.kindString](element, bundle); } else { diff --git a/scripts/index.ts b/scripts/index.ts index 4d7f1dbfb..5ebc05dfe 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -30,5 +30,5 @@ async function main() { main() .then(() => process.exit(0)); -// I think LowDB is keeping the process alive after it should die +// Something is keeping the process alive after it should die // but I haven't found how to close it so process.exit will have to do From 011960429b8a0bfdfc2f5d5c4842cc19fecc0ac0 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 15:44:53 +0800 Subject: [PATCH 29/51] Update tabs.ts Tabs are now rebuild when their parent bundle was rebuilt --- scripts/build/tabs.ts | 59 +++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts index a455baf19..65b6bf304 100644 --- a/scripts/build/tabs.ts +++ b/scripts/build/tabs.ts @@ -6,11 +6,11 @@ import { getDb, removeDuplicates, defaultConfig, - shouldBuildAll, BuildTask, } from './buildUtils'; import copy from './misc'; import { modules } from '../utilities'; +import { BUILD_PATH, SOURCE_PATH } from '../constants'; export const convertRawTab = (rawTab: string) => { const lastBracket = rawTab.lastIndexOf('('); @@ -21,21 +21,54 @@ export const convertRawTab = (rawTab: string) => { * Transpile tabs to the build folder */ export const buildTabs: BuildTask = async (db) => { - const isTabModifed = (tabName: string) => { - if (process.argv[3] === '--force') return true; - const timestamp = db.data.tabs[tabName] ?? 0; - return isFolderModified(`src/tabs/${tabName}`, timestamp); - }; + const getTabs = async () => { + const getAllTabs = () => removeDuplicates(Object.values(modules).flatMap(x => x.tabs)); + + // If forcing, just get all tabs + if (process.argv[3] === '--force') return getAllTabs(); + + try { + const tabBuildDir = await fs.readdir(`${BUILD_PATH}/tabs`); + + // If the tab build directory is empty, build all tabs + if (tabBuildDir.length === 0) return getAllTabs(); + } catch (error) { + + // If the tab build directory doesn't exist, build all tabs + if (error.code === 'ENOENT') return getAllTabs(); + throw error; + } + + const tabNames = new Set(); - const tabNames = removeDuplicates( - Object.values(modules) - .flatMap((x) => x.tabs), - ); + Object.entries(modules).forEach(([bundle, { tabs }]) => { + const bundleTimestamp = db.data.bundles[bundle] ?? 0; - const filteredTabs = shouldBuildAll('tabs') - ? tabNames - : tabNames.filter(isTabModifed); + // If bundle has no tabs, skip it + if (tabs.length === 0) return; + + // For each bundle, if it was modified, its tabs need to be rebuilt + if (!bundleTimestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, bundleTimestamp)) { + console.log(`${chalk.blue(bundle)} was modified, rebuilding it's tabs`); + tabs.forEach(tabNames.add); + + return; + } + + tabs.forEach(tabName => { + // Check if the tab itself was modified + const tabTimestamp = db.data.tabs[tabName]; + if (!tabTimestamp || isFolderModified(`${SOURCE_PATH}/tabs/${tabName}`, tabTimestamp)) { + tabNames.add(tabName); + } + }) + }) + + return [...tabNames]; + } + const filteredTabs = await getTabs(); + if (filteredTabs.length === 0) { console.log(chalk.greenBright('All tabs up to date')); return; From 5696de812293651b034f38ffe0e9917f94bc8e40 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 17:56:18 +0800 Subject: [PATCH 30/51] Updated scripts code --- jest.config.ts | 14 ++++++++++++ package.json | 7 +++--- scripts/build/tabs.ts | 6 +++--- scripts/index.ts | 2 ++ scripts/tsconfig.test.json | 1 + scripts/utilities.ts | 44 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 jest.config.ts diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 000000000..4faa89959 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,14 @@ +export default { + modulePaths:[ + '', + ], + // Module Name settings required to make chalk work with jest + moduleNameMapper: { + "#(.*)": "/node_modules/$1", + "lowdb": "/node_modules/lowdb/lib", + "steno": "/node_modules/steno/lib", + }, + transformIgnorePatterns: [ + 'node_modules/(?!=chalk)/' + ], +} \ No newline at end of file diff --git a/package.json b/package.json index c777eedce..a2d776091 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:tabs": "Transpile tabs into the `build/` folder and copy over `modules.json`", "build:docs": "Generate TypeDocs in the `build/documentation/` folder", "serve": "Start the HTTP server to serve all files in `build/`, with the same directory structure", - "scripts": "Run a script within the script server using ts-node", + "scripts": "Run a script within the scripts directory using ts-node", "dev": "Rebuild modules, then serve", "prepare": "Enable git hooks", "test": "Run unit tests", @@ -22,8 +22,9 @@ }, "scripts": { "create": "yarn scripts create", - "lint": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", - "lint:fix": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/ --fix", + "lint": " yarn lint:src && yarn lint:scripts", + "lint:src": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", + "lint:scripts": "./node_modules/.bin/eslint -c scripts/.eslintrc.js --ext \".ts, .tsx\" scripts/", "build": "yarn lint && yarn scripts build", "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:bundles", "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:tabs", diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts index 65b6bf304..d02610c9f 100644 --- a/scripts/build/tabs.ts +++ b/scripts/build/tabs.ts @@ -50,7 +50,7 @@ export const buildTabs: BuildTask = async (db) => { // For each bundle, if it was modified, its tabs need to be rebuilt if (!bundleTimestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, bundleTimestamp)) { console.log(`${chalk.blue(bundle)} was modified, rebuilding it's tabs`); - tabs.forEach(tabNames.add); + tabs.forEach(tabName => tabNames.add(tabName)); return; } @@ -84,11 +84,11 @@ export const buildTabs: BuildTask = async (db) => { const processTab = async (tabName: string) => { const result = await rollup({ ...defaultConfig, - input: `src/tabs/${tabName}/index.tsx`, + input: `${SOURCE_PATH}/tabs/${tabName}/index.tsx`, external: ['react', 'react-dom'], }); - const tabFile = `build/tabs/${tabName}.js`; + const tabFile = `${BUILD_PATH}/tabs/${tabName}.js`; const { output: rollupOutput } = await result.generate({ file: tabFile, format: 'iife', diff --git a/scripts/index.ts b/scripts/index.ts index 5ebc05dfe..9a921b984 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -4,6 +4,7 @@ import buildTabs from './build/tabs'; import buildDocs from './build/docs'; import create from './templates'; import chalk from 'chalk'; +import { testFunc } from './utilities'; const tasks = { build, @@ -11,6 +12,7 @@ const tasks = { 'build:tabs': buildTabs, 'build:docs': buildDocs, create, + testFunc, }; async function main() { diff --git a/scripts/tsconfig.test.json b/scripts/tsconfig.test.json index 62e0985f7..163cc73da 100644 --- a/scripts/tsconfig.test.json +++ b/scripts/tsconfig.test.json @@ -1,3 +1,4 @@ { + "extends": "./tsconfig.json", "include": ["./**/__tests__/**.ts"], } \ No newline at end of file diff --git a/scripts/utilities.ts b/scripts/utilities.ts index d9e5f3d04..ff8466e16 100644 --- a/scripts/utilities.ts +++ b/scripts/utilities.ts @@ -17,3 +17,47 @@ export const modules = _modules as ModuleManifest; * Function to replace the functionality of `__dirname` in CommonJS modules */ export const cjsDirname = (url: string) => dirname(fileURLToPath(url)); + +export async function* asCompleted(promises: Promise[]) { + /** + * Named after the C# TaskCompletionSource + */ + class TCS { + public isResolved: boolean; + + constructor( + public readonly promise: Promise, + ) { + this.isResolved = false; + + promise + .catch(() => { this.isResolved = true; }) + .then(() => { this.isResolved = true; }); + } + } + + const tcs = promises.map((promise) => new TCS(promise)); + + while (tcs.length > 0) { + // eslint-disable-next-line no-await-in-loop + await Promise.race(tcs.map((each) => each.promise)); + const index = tcs.findIndex((each) => each.isResolved); + const [toYield] = tcs.splice(index, 1); + + yield toYield.promise; + } +} + +export const testFunc = async () => { + const sleep = (d: number) => new Promise((r) => setTimeout(r, d)); + + const promises = Array.from({ length: 10 }, async (_, i) => { + const duration = Math.floor(Math.random() * 1000) + 1; + await sleep(duration); + return `Task ${i}: Duration: ${duration}ms`; + }); + + for await (const promise of asCompleted(promises)) { + console.log(await promise); + } +}; From ccf1009038aeba763c90dcf5b02fe2416eaf1830 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Mon, 15 Aug 2022 17:59:47 +0800 Subject: [PATCH 31/51] Moved jest config out of package.json --- jest.config.ts | 5 +++++ package.json | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 4faa89959..626ad435c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,4 +1,9 @@ export default { + roots: [ + "/src/bundles", + "/src/tabs", + "/scripts" + ], modulePaths:[ '', ], diff --git a/package.json b/package.json index a2d776091..f9e64f78b 100644 --- a/package.json +++ b/package.json @@ -101,12 +101,5 @@ "regl": "^2.1.0", "tslib": "^2.3.1", "typedoc-default-themes": "^0.12.10" - }, - "jest": { - "roots": [ - "/src/bundles", - "/src/tabs", - "/scripts" - ] } } From 31fc3111444d5ea084813ed5f273ce62ca438897 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Wed, 17 Aug 2022 00:54:14 +0800 Subject: [PATCH 32/51] Removed build:tabs command Since tabs may rely on the underlying bundle, tabs will now be rebuilt every time it's corresponding bundle is rebuilt --- package.json | 5 +- scripts/__tests__/tabs.test.ts | 2 +- scripts/build/bundles.ts | 66 ------------ scripts/build/docs/index.ts | 2 +- scripts/build/index.ts | 180 ++++++++++++++++++++++++++++++--- scripts/build/tabs.ts | 117 --------------------- scripts/index.ts | 6 -- scripts/utilities.ts | 14 --- yarn.lock | 5 + 9 files changed, 176 insertions(+), 221 deletions(-) delete mode 100644 scripts/build/bundles.ts delete mode 100644 scripts/build/tabs.ts diff --git a/package.json b/package.json index f9e64f78b..f182f7c62 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,6 @@ "lint": "Check code for eslint warnings and errors", "lint:fix": "Lint code and fix automatically fixable problems", "build": "Lint code, then build modules and documentation", - "build:bundles": "Transpile modules into the `build/` folder and copy over `modules.json`", - "build:tabs": "Transpile tabs into the `build/` folder and copy over `modules.json`", "build:docs": "Generate TypeDocs in the `build/documentation/` folder", "serve": "Start the HTTP server to serve all files in `build/`, with the same directory structure", "scripts": "Run a script within the scripts directory using ts-node", @@ -26,8 +24,6 @@ "lint:src": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:scripts": "./node_modules/.bin/eslint -c scripts/.eslintrc.js --ext \".ts, .tsx\" scripts/", "build": "yarn lint && yarn scripts build", - "build:bundles": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:bundles", - "build:tabs": "./node_modules/.bin/tsc --project src/tsconfig.json && yarn scripts build:tabs", "build:docs": "yarn scripts build:docs", "serve": "http-server --cors=* -c-1 -p 8022 ./build", "dev": "yarn build && yarn serve", @@ -56,6 +52,7 @@ "babel-jest": "^26.6.3", "babel-plugin-transform-import-meta": "^2.2.0", "chalk": "^5.0.1", + "commander": "^9.4.0", "cross-env": "^7.0.3", "eslint": "^8.21.0", "eslint-config-airbnb": "^19.0.4", diff --git a/scripts/__tests__/tabs.test.ts b/scripts/__tests__/tabs.test.ts index c17b30866..c752c5e27 100644 --- a/scripts/__tests__/tabs.test.ts +++ b/scripts/__tests__/tabs.test.ts @@ -1,4 +1,4 @@ -import { convertRawTab } from '../build/tabs'; +import { convertRawTab } from '../build'; describe('Testing raw tab processing', () => { test('Converts React tab properly', () => { diff --git a/scripts/build/bundles.ts b/scripts/build/bundles.ts deleted file mode 100644 index bf1f7c3bc..000000000 --- a/scripts/build/bundles.ts +++ /dev/null @@ -1,66 +0,0 @@ -import chalk from 'chalk'; -import { rollup } from 'rollup'; -import { modules } from '../utilities'; -import copy from './misc'; -import { - BuildTask, - defaultConfig, - getDb, - isFolderModified, - shouldBuildAll, -} from './buildUtils'; -import { BUILD_PATH, SOURCE_PATH } from '../constants'; - -/** - * Transpile bundles to the build folder - */ -export const buildBundles: BuildTask = async (db) => { - const isBundleModifed = (bundle: string) => { - if (process.argv[3] === '--force') return true; - const timestamp = db.data.bundles[bundle] ?? 0; - return isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, timestamp); - }; - - const bundleNames = Object.keys(modules); - const filteredBundles = shouldBuildAll('bundles') - ? bundleNames - : bundleNames.filter(isBundleModifed); - - if (filteredBundles.length === 0) { - console.log('All bundles up to date'); - return null; - } - - console.log(chalk.greenBright('Building bundles for the following modules:')); - console.log( - filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`) - .join('\n'), - ); - - const buildTime = new Date() - .getTime(); - - const processBundle = async (bundle: string) => { - const result = await rollup({ - ...defaultConfig, - input: `${SOURCE_PATH}/bundles/${bundle}/index.ts`, - }); - - await result.write({ - file: `${BUILD_PATH}/bundles/${bundle}.js`, - format: 'iife', - }); - await result.close(); - - db.data.bundles[bundle] = buildTime - }; - - await Promise.all(filteredBundles.map(processBundle)); - await db.write(); -}; - -export default async () => { - const db = await getDb(); - await buildBundles(db); - await copy(); -}; diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index 2e51c4c52..8745123ac 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -92,7 +92,7 @@ export const buildJsons: BuildTask = async (db) => { : bundleNames.filter(isBundleModifed); if (filteredBundles.length === 0) { - console.log('Documentation up to date'); + console.log(chalk.greenBright('Documentation up to date')); return; } diff --git a/scripts/build/index.ts b/scripts/build/index.ts index 8e4826dea..96ae1b181 100644 --- a/scripts/build/index.ts +++ b/scripts/build/index.ts @@ -1,16 +1,172 @@ +import fs from 'fs/promises'; +import { Command } from 'commander'; import copy from './misc'; -import { buildBundles } from './bundles'; -import { buildTabs } from './tabs'; -import { buildDocs, buildJsons } from './docs'; -import { getDb } from './buildUtils'; +import { BuildTask, defaultConfig, getDb, isFolderModified, removeDuplicates } from './buildUtils'; +import { modules } from '../utilities'; +import { BUILD_PATH, SOURCE_PATH } from '../constants'; +import chalk from 'chalk'; +import { rollup } from 'rollup'; +import { buildDocs } from './docs'; + +const getArgs = () => { + const argparser = new Command(); + + argparser.option('-f, --force', 'Build all bundles regardless of build status') + argparser.option('-m, --module ', 'Build specified bundles regardless of build status') + + return argparser.parse().opts(); +} + +export const convertRawTab = (rawTab: string) => { + const lastBracket = rawTab.lastIndexOf('('); + return `${rawTab.substring(0, lastBracket)})`; +}; + +export const build: BuildTask = async (db) => { + const opts = getArgs(); + + const getBundles = async (): Promise<[string, string][]> => { + const shouldBuildBundle = (bundleName: string): false | string => { + // Folder was modified + const timestamp = db.data.bundles[bundleName] ?? 0; + if( !timestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundleName}`, timestamp)) { + return 'Outdated build' + } + return false; + } + const bundleNames = Object.keys(modules); + + if (opts.force) return bundleNames.map(bundle => [bundle, '--force specified']); + + if (opts.module) { + const unknowns = opts.module.filter(bundleName => !bundleNames.includes(bundleName)); + if (unknowns.length > 0) { + throw new Error(`Unknown modules: ${unknowns.join(', ')}`); + } + + return opts.module.map(bundle => [bundle, 'Specified by --module']) as [string, string][]; + } + + try { + // Bundles build directory is empty or doesn't exist + const bundlesDir = await fs.readdir(`${BUILD_PATH}/bundles`); + if (bundlesDir.length === 0) return bundleNames.map(bundle => [bundle, 'Bundles build directory is empty']); + } catch (error) { + if (error.code === 'ENOENT') return bundleNames.map(bundle => [bundle, 'Bundles build directory missing']); + throw error; + } + + return bundleNames.map(bundleName => { + const result = shouldBuildBundle(bundleName); + return result === false ? result : [bundleName, result]; + }).filter(x => x !== false) as unknown as [string, string][]; + } + + const bundlesWithReason = await getBundles(); + + if (bundlesWithReason.length === 0) { + console.log(chalk.greenBright('All bundles up to date')); + } else { + console.log('Building the following bundles:') + console.log(bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`).join('\n')); + } + + const bundlesToBuild = bundlesWithReason.map(([bundle]) => bundle); + + const getTabs = async (): Promise<[string, string][]> => { + const getAllTabs = (reason: string): [string, string][] => removeDuplicates(Object.values(modules).flatMap(x => x.tabs)).map(tab => [tab, reason]); + + // If forcing, just get all tabs + if (opts.force) return getAllTabs('--force specified'); + + try { + const tabBuildDir = await fs.readdir(`${BUILD_PATH}/tabs`); + + // If the tab build directory is empty, build all tabs + if (tabBuildDir.length === 0) return getAllTabs('Tabs build directory is empty'); + } catch (error) { + + // If the tab build directory doesn't exist, build all tabs + if (error.code === 'ENOENT') return getAllTabs('Tabs build directory is missing'); + throw error; + } + + const tabNames = {} as { [name: string]: string }; + + // Add tabs for bundles that we are rebuilding + bundlesToBuild.forEach((bundleName) => modules[bundleName].tabs.forEach(tabName => { tabNames[tabName] = `${bundleName} bundle is being rebuilt`; })); + + getAllTabs('').forEach(([tabName]) => { + if (tabNames[tabName]) return; + + const timestamp = db.data.tabs[tabName] ?? 0; + if (!timestamp || isFolderModified(`${SOURCE_PATH}/tabs/${tabName}`, timestamp)) { + tabNames[tabName] = 'Outdated buiid'; + } + }) + + return Object.entries(tabNames); + } + + console.log(); + + const tabsWithReason = await getTabs(); + if (tabsWithReason.length === 0) { + console.log(chalk.greenBright('All tabs up to date')); + } else { + console.log('Building the following tabs:') + console.log(tabsWithReason.map(([tabName, reason]) => `• ${chalk.blueBright(tabName)}: ${reason}`).join('\n')); + } + + const tabsToBuild = tabsWithReason.map(([tabName]) => tabName); + const buildTime = new Date().getTime(); + + const bundlePromises = bundlesToBuild.map(async bundle => { + const rollupBundle = await rollup({ + ...defaultConfig, + input: `${SOURCE_PATH}/bundles/${bundle}/index.ts`, + }) + + await rollupBundle.write({ + file: `${BUILD_PATH}/bundles/${bundle}.js`, + format: 'iife', + }) + await rollupBundle.close(); + + db.data.bundles[bundle] = buildTime; + }); + + const tabPromises = tabsToBuild.map(async tabName => { + const rollupBundle = await rollup({ + ...defaultConfig, + input: `${SOURCE_PATH}/tabs/${tabName}/index.tsx`, + external: ['react', 'react-dom'], + }); + + const tabFile = `${BUILD_PATH}/tabs/${tabName}.js`; + const { output: rollupOutput } = await rollupBundle.generate({ + file: tabFile, + format: 'iife', + globals: { + 'react': 'React', + 'react-dom': 'ReactDom', + }, + }); + + // Only one chunk should be generated + const rawTab = rollupOutput[0].code; + await fs.writeFile(tabFile, convertRawTab(rawTab)); + await rollupBundle.close(); + + db.data.tabs[tabName] = buildTime + }) + + await Promise.all(bundlePromises.concat(tabPromises)); + await copy(); + await db.write(); +} export default async () => { const db = await getDb(); - await Promise.all([ - buildBundles(db), - buildTabs(db), - buildDocs(db) - .then(() => buildJsons(db)), - ]); - await copy(); -}; + await Promise.all([build(db), buildDocs(db)]); +} \ No newline at end of file diff --git a/scripts/build/tabs.ts b/scripts/build/tabs.ts deleted file mode 100644 index d02610c9f..000000000 --- a/scripts/build/tabs.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { rollup } from 'rollup'; -import chalk from 'chalk'; -import { promises as fs } from 'fs'; -import { - isFolderModified, - getDb, - removeDuplicates, - defaultConfig, - BuildTask, -} from './buildUtils'; -import copy from './misc'; -import { modules } from '../utilities'; -import { BUILD_PATH, SOURCE_PATH } from '../constants'; - -export const convertRawTab = (rawTab: string) => { - const lastBracket = rawTab.lastIndexOf('('); - return `${rawTab.substring(0, lastBracket)})`; -}; - -/** - * Transpile tabs to the build folder - */ -export const buildTabs: BuildTask = async (db) => { - const getTabs = async () => { - const getAllTabs = () => removeDuplicates(Object.values(modules).flatMap(x => x.tabs)); - - // If forcing, just get all tabs - if (process.argv[3] === '--force') return getAllTabs(); - - try { - const tabBuildDir = await fs.readdir(`${BUILD_PATH}/tabs`); - - // If the tab build directory is empty, build all tabs - if (tabBuildDir.length === 0) return getAllTabs(); - } catch (error) { - - // If the tab build directory doesn't exist, build all tabs - if (error.code === 'ENOENT') return getAllTabs(); - throw error; - } - - const tabNames = new Set(); - - Object.entries(modules).forEach(([bundle, { tabs }]) => { - const bundleTimestamp = db.data.bundles[bundle] ?? 0; - - // If bundle has no tabs, skip it - if (tabs.length === 0) return; - - // For each bundle, if it was modified, its tabs need to be rebuilt - if (!bundleTimestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, bundleTimestamp)) { - console.log(`${chalk.blue(bundle)} was modified, rebuilding it's tabs`); - tabs.forEach(tabName => tabNames.add(tabName)); - - return; - } - - tabs.forEach(tabName => { - // Check if the tab itself was modified - const tabTimestamp = db.data.tabs[tabName]; - if (!tabTimestamp || isFolderModified(`${SOURCE_PATH}/tabs/${tabName}`, tabTimestamp)) { - tabNames.add(tabName); - } - }) - }) - - return [...tabNames]; - } - - const filteredTabs = await getTabs(); - - if (filteredTabs.length === 0) { - console.log(chalk.greenBright('All tabs up to date')); - return; - } - - console.log(chalk.greenBright('Building the following tabs:')); - console.log(filteredTabs.map((x) => `• ${chalk.blue(x)}`) - .join('\n')); - - const buildTime = new Date() - .getTime(); - - const processTab = async (tabName: string) => { - const result = await rollup({ - ...defaultConfig, - input: `${SOURCE_PATH}/tabs/${tabName}/index.tsx`, - external: ['react', 'react-dom'], - }); - - const tabFile = `${BUILD_PATH}/tabs/${tabName}.js`; - const { output: rollupOutput } = await result.generate({ - file: tabFile, - format: 'iife', - globals: { - 'react': 'React', - 'react-dom': 'ReactDom', - }, - }); - - // Only one chunk should be generated - const rawTab = rollupOutput[0].code; - await fs.writeFile(tabFile, convertRawTab(rawTab)); - await result.close(); - - db.data.tabs[tabName] = buildTime - }; - - await Promise.all(filteredTabs.map(processTab)); - await db.write(); -}; - -export default async () => { - const db = await getDb(); - await buildTabs(db); - await copy(); -}; diff --git a/scripts/index.ts b/scripts/index.ts index 9a921b984..04c4bdb1f 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -1,18 +1,12 @@ import build from './build'; -import buildBundles from './build/bundles'; -import buildTabs from './build/tabs'; import buildDocs from './build/docs'; import create from './templates'; import chalk from 'chalk'; -import { testFunc } from './utilities'; const tasks = { build, - 'build:bundles': buildBundles, - 'build:tabs': buildTabs, 'build:docs': buildDocs, create, - testFunc, }; async function main() { diff --git a/scripts/utilities.ts b/scripts/utilities.ts index ff8466e16..e5c2e70f2 100644 --- a/scripts/utilities.ts +++ b/scripts/utilities.ts @@ -47,17 +47,3 @@ export async function* asCompleted(promises: Promise[]) { yield toYield.promise; } } - -export const testFunc = async () => { - const sleep = (d: number) => new Promise((r) => setTimeout(r, d)); - - const promises = Array.from({ length: 10 }, async (_, i) => { - const duration = Math.floor(Math.random() * 1000) + 1; - await sleep(duration); - return `Task ${i}: Duration: ${duration}ms`; - }); - - for await (const promise of asCompleted(promises)) { - console.log(await promise); - } -}; diff --git a/yarn.lock b/yarn.lock index 78eaa5147..11790c4a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2955,6 +2955,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.0.tgz#bc4a40918fefe52e22450c111ecd6b7acce6f11c" + integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" From 3fea1bff42c93bd6ca981c3e82f8109d7d84c1b2 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Wed, 17 Aug 2022 04:44:17 +0800 Subject: [PATCH 33/51] Use commander to parse CLI arguments --- package.json | 2 +- scripts/build/build.ts | 227 ++++++++++++++++++++++++++++++++++++ scripts/build/buildUtils.ts | 36 +++--- scripts/build/docs/index.ts | 119 +++++++++++++------ scripts/build/index.ts | 194 +++++------------------------- scripts/constants.ts | 6 +- scripts/index.ts | 53 ++++++--- 7 files changed, 394 insertions(+), 243 deletions(-) create mode 100644 scripts/build/build.ts diff --git a/package.json b/package.json index f182f7c62..b98dea9de 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint:src": "./node_modules/.bin/eslint -c src/.eslintrc.js --ext \".ts, .tsx\" src/", "lint:scripts": "./node_modules/.bin/eslint -c scripts/.eslintrc.js --ext \".ts, .tsx\" scripts/", "build": "yarn lint && yarn scripts build", - "build:docs": "yarn scripts build:docs", + "build:docs": "yarn scripts build docs", "serve": "http-server --cors=* -c-1 -p 8022 ./build", "dev": "yarn build && yarn serve", "scripts": "cross-env TS_NODE_PROJECT=scripts/tsconfig.json ts-node scripts/index.ts", diff --git a/scripts/build/build.ts b/scripts/build/build.ts new file mode 100644 index 000000000..7be6cbb52 --- /dev/null +++ b/scripts/build/build.ts @@ -0,0 +1,227 @@ +import fs from 'fs'; +import fsPromises from 'fs/promises'; +import { checkForUnknowns, DBType, defaultConfig, EntryWithReason, getDb, isFolderModified, Opts, removeDuplicates } from './buildUtils'; +import { modules } from '../utilities'; +import { BUILD_PATH, SOURCE_PATH } from '../constants'; +import chalk from 'chalk'; +import { rollup } from 'rollup'; +import memoize from 'lodash/memoize'; +import type { Low } from 'lowdb/lib'; +import copy from './misc'; + +type Config = { + bundlesWithReason: EntryWithReason[]; + tabsWithReason: EntryWithReason[]; +}; + +export const convertRawTab = (rawTab: string) => { + const lastBracket = rawTab.lastIndexOf('('); + return `${rawTab.substring(0, lastBracket)})`; +}; + +/** + * Determine which bundles and tabs to build + */ +export const getBundlesAndTabs = async (db: Low, opts: Opts): Promise => { + const getBundles = async (): Promise => { + const shouldBuildBundle = (bundleName: string): false | string => { + if (!fs.existsSync(`${BUILD_PATH}/bundles/${bundleName}.js`)) { + return 'Bundle file missing from build folder'; + } + + // Folder was modified + const timestamp = db.data.bundles[bundleName] ?? 0; + if (!timestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundleName}`, timestamp)) { + return 'Outdated build'; + } + return false; + }; + const bundleNames = Object.keys(modules); + + if (opts.force) return bundleNames.map((bundle) => [bundle, '--force specified']); + + if (opts.modules) { + const unknowns = checkForUnknowns(opts.modules, bundleNames); + if (unknowns.length > 0) { + throw new Error(`Unknown modules: ${unknowns.join(', ')}`); + } + + return opts.modules.map((bundle) => [bundle, 'Specified by --module']) as [string, string][]; + } + + try { + // Bundles build directory is empty or doesn't exist + const bundlesDir = await fsPromises.readdir(`${BUILD_PATH}/bundles`); + if (bundlesDir.length === 0) return bundleNames.map((bundle) => [bundle, 'Bundles build directory is empty']); + } catch (error) { + if (error.code === 'ENOENT') { + await fsPromises.mkdir(`${BUILD_PATH}/bundles`); + return bundleNames.map((bundle) => [bundle, 'Bundles build directory missing']); + } + throw error; + } + + return bundleNames.map((bundleName) => { + const result = shouldBuildBundle(bundleName); + return result === false ? result : [bundleName, result]; + }) + .filter((x) => x !== false) as unknown as [string, string][]; + }; + + const bundlesWithReason = await getBundles(); + + const getTabs = async (): Promise => { + // Use lodash to memoize so we don't have to keep performing this tab calculation + const getAllTabs = memoize(() => removeDuplicates(Object.values(modules) + .flatMap((bundle) => bundle.tabs))); + + // If forcing, just get all tabs + if (opts.force) { + return getAllTabs() + .map((tabName) => [tabName, '--force specified']); + } + + try { + const tabBuildDir = await fsPromises.readdir(`${BUILD_PATH}/tabs`); + + // If the tab build directory is empty, build all tabs + if (tabBuildDir.length === 0) { + return getAllTabs() + .map((tabName) => [tabName, 'Tabs build directory is empty']); + } + } catch (error) { + // If the tab build directory doesn't exist, build all tabs + if (error.code === 'ENOENT') { + await fsPromises.mkdir(`${BUILD_PATH}/tabs`); + return getAllTabs() + .map((tabName) => [tabName, 'Tabs build directory is missing']); + } + throw error; + } + + const tabNames = {} as { [name: string]: string }; + + // If tab was specified to be rebuilt, then rebuild + if (opts.tabs) { + const unknowns = checkForUnknowns(opts.tabs, getAllTabs()); + + if (unknowns.length > 0) throw new Error(`Unknown tabs: ${unknowns.join('\n')}`); + opts.tabs.forEach((tabName) => { + tabNames[tabName] = 'Specified by --tab'; + }); + } + + // Add tabs for bundles that we are rebuilding + bundlesWithReason.forEach(([bundleName]) => { + modules[bundleName].tabs.forEach((tabName) => { + if (!tabNames[tabName]) tabNames[tabName] = `${bundleName} bundle is being rebuilt`; + }); + }); + + getAllTabs() + .forEach((tabName) => { + if (tabNames[tabName]) return; + + if (!fs.existsSync(`${BUILD_PATH}/tabs/${tabName}.js`)) { + tabNames[tabName] = 'Tab file missing from build folder'; + return; + } + + const timestamp = db.data.tabs[tabName] ?? 0; + if (!timestamp || isFolderModified(`${SOURCE_PATH}/tabs/${tabName}`, timestamp)) { + tabNames[tabName] = 'Outdated buiid'; + } + }); + + return Object.entries(tabNames); + }; + + const tabsWithReason = await getTabs(); + + return { + bundlesWithReason, + tabsWithReason, + }; +}; + +/** + * Use rollup to rebuild bundles and tabs + */ +export const buildBundlesAndTabs = async (db: Low, { + bundlesWithReason, + tabsWithReason, +}: Config) => { + if (bundlesWithReason.length === 0) { + console.log(chalk.greenBright('All bundles up to date')); + } else { + console.log('Building the following bundles:'); + console.log(bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) + .join('\n')); + } + + const bundlesToBuild = bundlesWithReason.map(([bundle]) => bundle); + console.log(); + + if (tabsWithReason.length === 0) { + console.log(chalk.greenBright('All tabs up to date')); + } else { + console.log('Building the following tabs:'); + console.log(tabsWithReason.map(([tabName, reason]) => `• ${chalk.blueBright(tabName)}: ${reason}`) + .join('\n')); + } + + const tabsToBuild = tabsWithReason.map(([tabName]) => tabName); + const buildTime = new Date() + .getTime(); + + const bundlePromises = bundlesToBuild.map(async (bundle) => { + const rollupBundle = await rollup({ + ...defaultConfig, + input: `${SOURCE_PATH}/bundles/${bundle}/index.ts`, + }); + + await rollupBundle.write({ + file: `${BUILD_PATH}/bundles/${bundle}.js`, + format: 'iife', + }); + await rollupBundle.close(); + + db.data.bundles[bundle] = buildTime; + }); + + const tabPromises = tabsToBuild.map(async (tabName) => { + const rollupBundle = await rollup({ + ...defaultConfig, + input: `${SOURCE_PATH}/tabs/${tabName}/index.tsx`, + external: ['react', 'react-dom'], + }); + + const tabFile = `${BUILD_PATH}/tabs/${tabName}.js`; + const { output: rollupOutput } = await rollupBundle.generate({ + file: tabFile, + format: 'iife', + globals: { + 'react': 'React', + 'react-dom': 'ReactDom', + }, + }); + + // Only one chunk should be generated + const rawTab = rollupOutput[0].code; + await fsPromises.writeFile(tabFile, convertRawTab(rawTab)); + await rollupBundle.close(); + + db.data.tabs[tabName] = buildTime; + }); + + await Promise.all(bundlePromises.concat(tabPromises)); + await db.write(); +}; + +export default async (opts: Opts) => { + const db = await getDb(); + const parsedOpts = await getBundlesAndTabs(db, opts); + + await buildBundlesAndTabs(db, parsedOpts); + await copy(); +}; diff --git a/scripts/build/buildUtils.ts b/scripts/build/buildUtils.ts index a9c948941..f4773aeff 100644 --- a/scripts/build/buildUtils.ts +++ b/scripts/build/buildUtils.ts @@ -12,7 +12,7 @@ import { Low, JSONFile } from 'lowdb'; import { DATABASE_NAME, NODE_MODULES_PATTERN, - SOURCE_PATTERN, + SOURCE_PATH, SUPPRESSED_WARNINGS, } from '../constants'; import { cjsDirname } from '../utilities'; @@ -33,7 +33,7 @@ export const defaultConfig = { babel({ babelHelpers: 'bundled', extensions: ['.ts', '.tsx'], - include: [SOURCE_PATTERN], + include: [`${SOURCE_PATH}/**`], }), rollupResolve({ // Source Academy's modules run in a browser environment. The default setting (false) may @@ -100,6 +100,8 @@ export function getDbPath() { return join(cjsDirname(import.meta.url), DATABASE_NAME); } +export const checkForUnknowns = (inputs: T[], existing: T[]) => inputs.filter((each) => !existing.includes(each)); + const DBKeys = ['jsons', 'bundles', 'tabs'] as const; type ObjectFromList, V = string> = { @@ -110,7 +112,16 @@ export type DBType = { docs: number; } & ObjectFromList +}>; + +export type EntryWithReason = [string, string]; + +export type Opts = Partial<{ + force: boolean; + modules: string[]; + tabs: string[]; + jsons: string[]; +}>; /** * Get a new Lowdb instance @@ -125,8 +136,8 @@ export async function getDb() { docs: 0, jsons: {}, bundles: {}, - tabs: {} - } + tabs: {}, + }; } return db; } @@ -136,18 +147,3 @@ export type BuildTask = (db: Low) => Promise; export function removeDuplicates(arr: T[]) { return [...new Set(arr)]; } - -/** - * Checks if the given output directory is empty, to determine - * if the given build script should execute regardless of the last build time - */ -export const shouldBuildAll = (outputDir: string) => { - if (process.argv[3] === '--force') return true; - - try { - return fs.readdirSync(`build/${outputDir}`).length === 0; - } catch (error) { - if (error.code === 'ENOENT') return true; - throw error; - } -}; diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index 8745123ac..f471fe9c5 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -1,15 +1,19 @@ -import { constants as fsConstants, promises as fs } from 'fs'; +import fs, { constants as fsConstants, promises as fsPromises } from 'fs'; import * as typedoc from 'typedoc'; import chalk from 'chalk'; import drawdown from './drawdown'; import { isFolderModified, getDb, - shouldBuildAll, BuildTask, + DBType, + checkForUnknowns, + EntryWithReason, + Opts, } from '../buildUtils'; import { cjsDirname, modules as manifest } from '../../utilities'; import { BUILD_PATH, SOURCE_PATH } from '../../constants'; +import type { Low } from 'lowdb/lib'; /** * Convert each element type (e.g. variable, function) to its respective HTML docstring @@ -77,21 +81,59 @@ const parsers: { }, }; +type JsonOpts = { + force?: boolean; + jsons: string[]; +}; + /** - * Build the json documentation for the specified modules + * Determine which json files to build */ -export const buildJsons: BuildTask = async (db) => { - const isBundleModifed = (bundle: string) => { - const timestamp = db.data.jsons[bundle] ?? 0; - return isFolderModified(`${SOURCE_PATH}/bundles/${bundle}`, timestamp); - }; - +export const getJsonsToBuild = async (db: Low, opts: JsonOpts): Promise => { const bundleNames = Object.keys(manifest); - const filteredBundles = shouldBuildAll('jsons') - ? bundleNames - : bundleNames.filter(isBundleModifed); - if (filteredBundles.length === 0) { + try { + const docsDir = await fsPromises.readdir(`${BUILD_PATH}/jsons`); + if (docsDir.length === 0) return bundleNames.map((bundleName) => [bundleName, 'JSONs build directory empty']); + } catch (error) { + if (error.code === 'ENOENT') { + await fsPromises.mkdir(`${BUILD_PATH}/jsons`); + return bundleNames.map((bundleName) => [bundleName, 'JSONs build directory missing']); + } + throw error; + } + + if (opts.force) { + return bundleNames.map((bundleName) => [bundleName, '--force specified']); + } + + if (opts.jsons) { + const unknowns = checkForUnknowns(opts.jsons, bundleNames); + if (unknowns.length > 0) throw new Error(`Unknown modules: ${unknowns.join(', ')}`); + + return opts.jsons.map((bundleName) => [bundleName, 'Specified by --module']); + } + + return bundleNames.map((bundleName) => { + if (!fs.existsSync(`${BUILD_PATH}/jsons/${bundleName}.json`)) { + return [bundleName, 'JSON missing from JSONS build directory']; + } + + const timestamp = db.data.jsons[bundleName]; + if (!timestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundleName}`, timestamp)) { + return [bundleName, 'Outdated build']; + } + + return false; + }) + .filter((x) => x !== false) as EntryWithReason[]; +}; + +/** + * Build the json documentation for the specified modules + */ +const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) => { + if (bundlesWithReason.length === 0) { console.log(chalk.greenBright('Documentation up to date')); return; } @@ -100,42 +142,40 @@ export const buildJsons: BuildTask = async (db) => { chalk.greenBright('Building documentation for the following bundles:'), ); console.log( - filteredBundles.map((bundle) => `• ${chalk.blueBright(bundle)}`) + bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) .join('\n'), ); + const bundleNames = bundlesWithReason.map(([bundle]) => bundle); + try { - await fs.access('build/jsons', fsConstants.F_OK); + await fsPromises.access('build/jsons', fsConstants.F_OK); } catch (_error) { - await fs.mkdir('build/jsons'); + await fsPromises.mkdir('build/jsons'); } const buildTime = new Date() .getTime(); - const docsFile = await fs.readFile('build/docs.json', 'utf-8'); + const docsFile = await fsPromises.readFile(`${BUILD_PATH}/docs.json`, 'utf-8'); const parsedJSON = JSON.parse(docsFile).children; if (!parsedJSON) { throw new Error('Failed to parse docs.json'); } - const bundles: [string, undefined | any[]][] = Object.keys(manifest) - .map((bundle) => { + await Promise.all( + bundleNames.map(async (bundle) => { const moduleDocs = parsedJSON.find((x) => x.name === bundle); if (!moduleDocs || !moduleDocs.children) { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}`, ); - return [bundle, undefined]; + return; } - return [bundle, moduleDocs.children]; - }); - - await Promise.all( - bundles.map(async ([bundle, docs]) => { + const docs = moduleDocs.children; if (!docs) return; // Run through each item in the bundle and run its parser @@ -153,12 +193,12 @@ export const buildJsons: BuildTask = async (db) => { }); // Then write that output to the bundles' respective json files - await fs.writeFile( + await fsPromises.writeFile( `${BUILD_PATH}/jsons/${bundle}.json`, JSON.stringify(output, null, 2), ); - db.data.jsons[bundle] = buildTime + db.data.jsons[bundle] = buildTime; }), ); @@ -172,7 +212,7 @@ export const buildJsons: BuildTask = async (db) => { * their documentation won't be properly included. Hence all modules have to be built at * the same time. */ -export const buildDocs: BuildTask = async (db) => { +const buildDocs: BuildTask = async (db) => { const app = new typedoc.Application(); app.options.addReader(new typedoc.TSConfigReader()); app.options.addReader(new typedoc.TypeDocReader()); @@ -191,27 +231,36 @@ export const buildDocs: BuildTask = async (db) => { const project = app.convert(); if (project) { - const docsTask = app.generateDocs(project, 'build/documentation'); - const jsonTask = app.generateJson(project, 'build/docs.json'); + const docsTask = app.generateDocs(project, `${BUILD_PATH}/documentation`); + const jsonTask = app.generateJson(project, `${BUILD_PATH}/docs.json`); await Promise.all([docsTask, jsonTask]); // For some reason typedoc's not working, so do a manual copy - await fs.copyFile( + await fsPromises.copyFile( `${cjsDirname(import.meta.url)}/README.md`, `${BUILD_PATH}/documentation/README.md`, ); db.data.docs = new Date() - .getTime() + .getTime(); await db.write(); } }; +export const buildDocsAndJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) => { + await buildDocs(db); + await buildJsons(db, bundlesWithReason); +}; + /** * Build both JSONS and HTML documentation */ -export default async () => { +export default async ({ jsons, force }: Opts) => { const db = await getDb(); - await buildDocs(db); - await buildJsons(db); + const jsonsToBuild = await getJsonsToBuild(db, { + jsons, + force, + }); + + await buildDocsAndJsons(db, jsonsToBuild); }; diff --git a/scripts/build/index.ts b/scripts/build/index.ts index 96ae1b181..fe4855171 100644 --- a/scripts/build/index.ts +++ b/scripts/build/index.ts @@ -1,172 +1,32 @@ -import fs from 'fs/promises'; -import { Command } from 'commander'; +import { buildBundlesAndTabs, getBundlesAndTabs } from './build'; +import { buildDocsAndJsons, getJsonsToBuild } from './docs'; +import { DBType, getDb, Opts } from './buildUtils'; +import type { Low } from 'lowdb/lib'; import copy from './misc'; -import { BuildTask, defaultConfig, getDb, isFolderModified, removeDuplicates } from './buildUtils'; -import { modules } from '../utilities'; -import { BUILD_PATH, SOURCE_PATH } from '../constants'; -import chalk from 'chalk'; -import { rollup } from 'rollup'; -import { buildDocs } from './docs'; -const getArgs = () => { - const argparser = new Command(); - - argparser.option('-f, --force', 'Build all bundles regardless of build status') - argparser.option('-m, --module ', 'Build specified bundles regardless of build status') - - return argparser.parse().opts(); -} - -export const convertRawTab = (rawTab: string) => { - const lastBracket = rawTab.lastIndexOf('('); - return `${rawTab.substring(0, lastBracket)})`; +const getThingsToBuild = async (db: Low, { force, modules, tabs, jsons }: Opts) => { + const [jsonOpts, bundleOpts] = await Promise.all([getJsonsToBuild(db, { + force, + jsons, + }), getBundlesAndTabs(db, { + force, + modules, + tabs, + })]); + + return { + force, + jsonOpts, + bundleOpts, + }; }; -export const build: BuildTask = async (db) => { - const opts = getArgs(); - - const getBundles = async (): Promise<[string, string][]> => { - const shouldBuildBundle = (bundleName: string): false | string => { - // Folder was modified - const timestamp = db.data.bundles[bundleName] ?? 0; - if( !timestamp || isFolderModified(`${SOURCE_PATH}/bundles/${bundleName}`, timestamp)) { - return 'Outdated build' - } - return false; - } - const bundleNames = Object.keys(modules); - - if (opts.force) return bundleNames.map(bundle => [bundle, '--force specified']); - - if (opts.module) { - const unknowns = opts.module.filter(bundleName => !bundleNames.includes(bundleName)); - if (unknowns.length > 0) { - throw new Error(`Unknown modules: ${unknowns.join(', ')}`); - } - - return opts.module.map(bundle => [bundle, 'Specified by --module']) as [string, string][]; - } - - try { - // Bundles build directory is empty or doesn't exist - const bundlesDir = await fs.readdir(`${BUILD_PATH}/bundles`); - if (bundlesDir.length === 0) return bundleNames.map(bundle => [bundle, 'Bundles build directory is empty']); - } catch (error) { - if (error.code === 'ENOENT') return bundleNames.map(bundle => [bundle, 'Bundles build directory missing']); - throw error; - } - - return bundleNames.map(bundleName => { - const result = shouldBuildBundle(bundleName); - return result === false ? result : [bundleName, result]; - }).filter(x => x !== false) as unknown as [string, string][]; - } - - const bundlesWithReason = await getBundles(); - - if (bundlesWithReason.length === 0) { - console.log(chalk.greenBright('All bundles up to date')); - } else { - console.log('Building the following bundles:') - console.log(bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`).join('\n')); - } - - const bundlesToBuild = bundlesWithReason.map(([bundle]) => bundle); - - const getTabs = async (): Promise<[string, string][]> => { - const getAllTabs = (reason: string): [string, string][] => removeDuplicates(Object.values(modules).flatMap(x => x.tabs)).map(tab => [tab, reason]); - - // If forcing, just get all tabs - if (opts.force) return getAllTabs('--force specified'); - - try { - const tabBuildDir = await fs.readdir(`${BUILD_PATH}/tabs`); - - // If the tab build directory is empty, build all tabs - if (tabBuildDir.length === 0) return getAllTabs('Tabs build directory is empty'); - } catch (error) { - - // If the tab build directory doesn't exist, build all tabs - if (error.code === 'ENOENT') return getAllTabs('Tabs build directory is missing'); - throw error; - } - - const tabNames = {} as { [name: string]: string }; - - // Add tabs for bundles that we are rebuilding - bundlesToBuild.forEach((bundleName) => modules[bundleName].tabs.forEach(tabName => { tabNames[tabName] = `${bundleName} bundle is being rebuilt`; })); - - getAllTabs('').forEach(([tabName]) => { - if (tabNames[tabName]) return; - - const timestamp = db.data.tabs[tabName] ?? 0; - if (!timestamp || isFolderModified(`${SOURCE_PATH}/tabs/${tabName}`, timestamp)) { - tabNames[tabName] = 'Outdated buiid'; - } - }) - - return Object.entries(tabNames); - } - - console.log(); - - const tabsWithReason = await getTabs(); - if (tabsWithReason.length === 0) { - console.log(chalk.greenBright('All tabs up to date')); - } else { - console.log('Building the following tabs:') - console.log(tabsWithReason.map(([tabName, reason]) => `• ${chalk.blueBright(tabName)}: ${reason}`).join('\n')); - } - - const tabsToBuild = tabsWithReason.map(([tabName]) => tabName); - const buildTime = new Date().getTime(); - - const bundlePromises = bundlesToBuild.map(async bundle => { - const rollupBundle = await rollup({ - ...defaultConfig, - input: `${SOURCE_PATH}/bundles/${bundle}/index.ts`, - }) - - await rollupBundle.write({ - file: `${BUILD_PATH}/bundles/${bundle}.js`, - format: 'iife', - }) - await rollupBundle.close(); - - db.data.bundles[bundle] = buildTime; - }); - - const tabPromises = tabsToBuild.map(async tabName => { - const rollupBundle = await rollup({ - ...defaultConfig, - input: `${SOURCE_PATH}/tabs/${tabName}/index.tsx`, - external: ['react', 'react-dom'], - }); - - const tabFile = `${BUILD_PATH}/tabs/${tabName}.js`; - const { output: rollupOutput } = await rollupBundle.generate({ - file: tabFile, - format: 'iife', - globals: { - 'react': 'React', - 'react-dom': 'ReactDom', - }, - }); - - // Only one chunk should be generated - const rawTab = rollupOutput[0].code; - await fs.writeFile(tabFile, convertRawTab(rawTab)); - await rollupBundle.close(); - - db.data.tabs[tabName] = buildTime - }) - - await Promise.all(bundlePromises.concat(tabPromises)); - await copy(); - await db.write(); -} - -export default async () => { +/** + * Build bundles, tabs, jsons and docs + */ +export default async (opts: Opts) => { const db = await getDb(); - await Promise.all([build(db), buildDocs(db)]); -} \ No newline at end of file + const { jsonOpts, bundleOpts } = await getThingsToBuild(db, opts); + await Promise.all([buildBundlesAndTabs(db, bundleOpts), buildDocsAndJsons(db, jsonOpts)]); + await copy(); +}; diff --git a/scripts/constants.ts b/scripts/constants.ts index 3bf3cce06..494cecc6d 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -3,9 +3,7 @@ export const SUPPRESSED_WARNINGS = ['MISSING_NAME_OPTION_FOR_IIFE_EXPORT']; export const DATABASE_NAME = 'database.json'; -export const SOURCE_PATH = './src/'; -export const BUILD_PATH = './build/'; +export const SOURCE_PATH = './src'; +export const BUILD_PATH = './build'; -export const SOURCE_PATTERN = `${SOURCE_PATH}**`; export const NODE_MODULES_PATTERN = './node_modules/**'; -export const MODULES_PATH = './modules.json'; diff --git a/scripts/index.ts b/scripts/index.ts index 04c4bdb1f..7cd44cd85 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -1,27 +1,48 @@ +import { promises as fs, constants as fsConstants } from 'fs'; import build from './build'; import buildDocs from './build/docs'; +import buildBundlesAndTabs from './build/build'; import create from './templates'; +import type { Opts } from './build/buildUtils'; +import { Command } from 'commander'; +import { BUILD_PATH } from './constants'; import chalk from 'chalk'; -const tasks = { - build, - 'build:docs': buildDocs, - create, +const buildTasks: { [name: string]: (opts: Opts) => Promise } = { + docs: buildDocs, + modules: buildBundlesAndTabs, }; async function main() { - if (process.argv.length < 3) { - console.log(chalk.green('Available tasks:')); - console.log(Object.keys(tasks) - .map((each) => `• ${each}`) - .join('\n')); - return; - } - - const task = tasks[process.argv[2]]; - - if (!task) console.error(chalk.redBright(`Unknown task: ${process.argv[2]}`)); - else await task(); + const parser = new Command(); + + parser.command('create', 'Interactive script for creating modules and tabs') + .action(create); + + parser.command('build') + .argument('[script]', 'Build task to execute') + .option('-f, --force', 'Force all files to be rebuilt') + .option('-m, --module ', 'Specify bundles to be rebuilt') + .option('-t, --tab ', 'Specify tabs to be rebuilt') + .option('-j, --jsons ', 'Specify jsons to be rebuilt') + .action(async (script: string, options: Opts) => { + if (script !== undefined && !buildTasks[script]) { + console.error(chalk.redBright(`Unknown task: ${script}`)); + return; + } + + try { + // Create the build folder if it doesn't already exist + await fs.access(BUILD_PATH, fsConstants.F_OK); + } catch (error) { + await fs.mkdir(BUILD_PATH); + } + + if (script === undefined) await build(options); + else await buildTasks[script](options); + }); + + await parser.parseAsync(); } main() From 2350229a9cf7ba75e2d3ed184069471103cbc172 Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Wed, 17 Aug 2022 21:03:43 +0800 Subject: [PATCH 34/51] Update index.ts --- scripts/build/docs/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index f471fe9c5..8d56e22d4 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -158,7 +158,7 @@ const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) .getTime(); const docsFile = await fsPromises.readFile(`${BUILD_PATH}/docs.json`, 'utf-8'); - const parsedJSON = JSON.parse(docsFile).children; + const parsedJSON = JSON.parse(docsFile)?.children; if (!parsedJSON) { throw new Error('Failed to parse docs.json'); @@ -166,18 +166,15 @@ const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) await Promise.all( bundleNames.map(async (bundle) => { - const moduleDocs = parsedJSON.find((x) => x.name === bundle); + const docs = parsedJSON.find((x) => x.name === bundle)?.children; - if (!moduleDocs || !moduleDocs.children) { + if (!docs) { console.warn( `${chalk.yellow('Warning:')} No documentation found for ${bundle}`, ); return; } - const docs = moduleDocs.children; - if (!docs) return; - // Run through each item in the bundle and run its parser const output: { [name: string]: string } = {}; docs.forEach((element) => { From 4d04c23c72a1924b53511f9a48c1abe94b5fa96d Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 18 Aug 2022 01:40:50 +0800 Subject: [PATCH 35/51] Added verbose option --- scripts/.eslintrc.js | 2 +- scripts/build/build.ts | 26 ++++++++++++++++++-------- scripts/build/buildUtils.ts | 1 + scripts/build/docs/index.ts | 24 +++++++++++++++--------- scripts/build/index.ts | 3 +-- scripts/build/misc.ts | 5 +++-- scripts/index.ts | 1 + 7 files changed, 40 insertions(+), 22 deletions(-) diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js index 0a493058d..70f7fe528 100644 --- a/scripts/.eslintrc.js +++ b/scripts/.eslintrc.js @@ -23,7 +23,7 @@ module.exports = { "overrides": [{ "files": ["./**/__tests__/**.test.ts"], "parserOptions": { - "project": "tsconfig.test.json", + "project": "./tsconfig.test.json", "tsconfigRootDir": __dirname, }, "extends": ["plugin:jest/recommended"], diff --git a/scripts/build/build.ts b/scripts/build/build.ts index 7be6cbb52..cf5acf26d 100644 --- a/scripts/build/build.ts +++ b/scripts/build/build.ts @@ -46,7 +46,7 @@ export const getBundlesAndTabs = async (db: Low, opts: Opts): Promise [bundle, 'Specified by --module']) as [string, string][]; + return opts.modules.map((bundle) => [bundle, 'Specified by --module']) as EntryWithReason[]; } try { @@ -65,7 +65,7 @@ export const getBundlesAndTabs = async (db: Low, opts: Opts): Promise x !== false) as unknown as [string, string][]; + .filter((x) => x !== false) as EntryWithReason[]; }; const bundlesWithReason = await getBundles(); @@ -150,13 +150,18 @@ export const getBundlesAndTabs = async (db: Low, opts: Opts): Promise, { bundlesWithReason, tabsWithReason, -}: Config) => { +}: Config, verbose: boolean) => { if (bundlesWithReason.length === 0) { console.log(chalk.greenBright('All bundles up to date')); } else { console.log('Building the following bundles:'); - console.log(bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) - .join('\n')); + if (verbose) { + console.log(bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) + .join('\n')); + } else { + console.log(bundlesWithReason.map(([bundle]) => `• ${chalk.blueBright(bundle)}`) + .join('\n')); + } } const bundlesToBuild = bundlesWithReason.map(([bundle]) => bundle); @@ -166,8 +171,13 @@ export const buildBundlesAndTabs = async (db: Low, { console.log(chalk.greenBright('All tabs up to date')); } else { console.log('Building the following tabs:'); - console.log(tabsWithReason.map(([tabName, reason]) => `• ${chalk.blueBright(tabName)}: ${reason}`) - .join('\n')); + if (verbose) { + console.log(tabsWithReason.map(([tabName, reason]) => `• ${chalk.blueBright(tabName)}: ${reason}`) + .join('\n')); + } else { + console.log(tabsWithReason.map(([tabName]) => `• ${chalk.blueBright(tabName)}`) + .join('\n')); + } } const tabsToBuild = tabsWithReason.map(([tabName]) => tabName); @@ -222,6 +232,6 @@ export default async (opts: Opts) => { const db = await getDb(); const parsedOpts = await getBundlesAndTabs(db, opts); - await buildBundlesAndTabs(db, parsedOpts); + await buildBundlesAndTabs(db, parsedOpts, opts.verbose); await copy(); }; diff --git a/scripts/build/buildUtils.ts b/scripts/build/buildUtils.ts index f4773aeff..88e596c06 100644 --- a/scripts/build/buildUtils.ts +++ b/scripts/build/buildUtils.ts @@ -118,6 +118,7 @@ export type EntryWithReason = [string, string]; export type Opts = Partial<{ force: boolean; + verbose: boolean; modules: string[]; tabs: string[]; jsons: string[]; diff --git a/scripts/build/docs/index.ts b/scripts/build/docs/index.ts index 8d56e22d4..f49173cb1 100644 --- a/scripts/build/docs/index.ts +++ b/scripts/build/docs/index.ts @@ -132,7 +132,7 @@ export const getJsonsToBuild = async (db: Low, opts: JsonOpts): Promise< /** * Build the json documentation for the specified modules */ -const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) => { +const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[], verbose: boolean) => { if (bundlesWithReason.length === 0) { console.log(chalk.greenBright('Documentation up to date')); return; @@ -141,10 +141,16 @@ const buildJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) console.log( chalk.greenBright('Building documentation for the following bundles:'), ); - console.log( - bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) - .join('\n'), - ); + + if (verbose) { + console.log( + bundlesWithReason.map(([bundle, reason]) => `• ${chalk.blueBright(bundle)}: ${reason}`) + .join('\n'), + ); + } else { + console.log(bundlesWithReason.map(([bundle]) => `• ${chalk.blueBright(bundle)}`) + .join('\n')); + } const bundleNames = bundlesWithReason.map(([bundle]) => bundle); @@ -244,20 +250,20 @@ const buildDocs: BuildTask = async (db) => { } }; -export const buildDocsAndJsons = async (db: Low, bundlesWithReason: EntryWithReason[]) => { +export const buildDocsAndJsons = async (db: Low, bundlesWithReason: EntryWithReason[], verbose: boolean) => { await buildDocs(db); - await buildJsons(db, bundlesWithReason); + await buildJsons(db, bundlesWithReason, verbose); }; /** * Build both JSONS and HTML documentation */ -export default async ({ jsons, force }: Opts) => { +export default async ({ verbose, jsons, force }: Opts) => { const db = await getDb(); const jsonsToBuild = await getJsonsToBuild(db, { jsons, force, }); - await buildDocsAndJsons(db, jsonsToBuild); + await buildDocsAndJsons(db, jsonsToBuild, verbose); }; diff --git a/scripts/build/index.ts b/scripts/build/index.ts index fe4855171..fc4966699 100644 --- a/scripts/build/index.ts +++ b/scripts/build/index.ts @@ -15,7 +15,6 @@ const getThingsToBuild = async (db: Low, { force, modules, tabs, jsons } })]); return { - force, jsonOpts, bundleOpts, }; @@ -27,6 +26,6 @@ const getThingsToBuild = async (db: Low, { force, modules, tabs, jsons } export default async (opts: Opts) => { const db = await getDb(); const { jsonOpts, bundleOpts } = await getThingsToBuild(db, opts); - await Promise.all([buildBundlesAndTabs(db, bundleOpts), buildDocsAndJsons(db, jsonOpts)]); + await Promise.all([buildBundlesAndTabs(db, bundleOpts, opts.verbose), buildDocsAndJsons(db, jsonOpts, opts.verbose)]); await copy(); }; diff --git a/scripts/build/misc.ts b/scripts/build/misc.ts index 83f2ac6f0..11860278b 100644 --- a/scripts/build/misc.ts +++ b/scripts/build/misc.ts @@ -1,14 +1,15 @@ import { promises as fs } from 'fs'; +import { BUILD_PATH } from '../constants'; import { getDbPath } from './buildUtils'; /** * Copy `modules.json` to the build folder */ -export const copyModules = () => fs.copyFile('modules.json', 'build/modules.json'); +export const copyModules = () => fs.copyFile('modules.json', `${BUILD_PATH}/modules.json`); /** * Copy `database.json` to the build folder */ -export const copyDatabase = () => fs.copyFile(getDbPath(), 'build/database.json'); +export const copyDatabase = () => fs.copyFile(getDbPath(), `${BUILD_PATH}/database.json`); export default () => Promise.all([copyModules(), copyDatabase()]); diff --git a/scripts/index.ts b/scripts/index.ts index 7cd44cd85..ce6adbd68 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -25,6 +25,7 @@ async function main() { .option('-m, --module ', 'Specify bundles to be rebuilt') .option('-t, --tab ', 'Specify tabs to be rebuilt') .option('-j, --jsons ', 'Specify jsons to be rebuilt') + .option('-v, --verbose', 'Enable verbose information') .action(async (script: string, options: Opts) => { if (script !== undefined && !buildTasks[script]) { console.error(chalk.redBright(`Unknown task: ${script}`)); From fbc6b6724e79e98fdb4fab914e0511327a3d954c Mon Sep 17 00:00:00 2001 From: leeyi45 Date: Thu, 18 Aug 2022 02:02:33 +0800 Subject: [PATCH 36/51] Updated linting --- src/bundles/copy_gc/index.ts | 24 +- src/bundles/copy_gc/types.ts | 64 +- src/bundles/csg/constants.ts | 52 +- src/bundles/csg/core.ts | 38 +- src/bundles/csg/functions.ts | 1665 ++++++------- src/bundles/csg/index.ts | 306 +-- src/bundles/csg/renderer.ts | 744 +++--- src/bundles/csg/types.ts | 698 +++--- src/bundles/csg/utilities.ts | 678 +++--- src/bundles/curve/curves_webgl.ts | 913 ++++--- src/bundles/curve/functions.ts | 1688 ++++++------- src/bundles/curve/index.ts | 19 +- src/bundles/curve/types.ts | 125 +- src/bundles/game/functions.ts | 3020 ++++++++++++------------ src/bundles/game/index.ts | 16 +- src/bundles/game/types.ts | 82 +- src/bundles/mark_sweep/index.ts | 24 +- src/bundles/mark_sweep/types.ts | 64 +- src/bundles/pix_n_flix/constants.ts | 26 +- src/bundles/pix_n_flix/functions.ts | 1457 ++++++------ src/bundles/rune/functions.ts | 2048 ++++++++-------- src/bundles/rune/index.ts | 282 +-- src/bundles/rune/rune.ts | 838 ++++--- src/bundles/rune/runes_ops.ts | 720 +++--- src/bundles/rune/runes_webgl.ts | 326 +-- src/bundles/scrabble/functions.ts | 2 +- src/bundles/sound/functions.ts | 1882 ++++++++------- src/bundles/sound/index.ts | 214 +- src/bundles/sound/list.ts | 744 +++--- src/bundles/sound/riffwave.ts | 38 +- src/bundles/sound/types.ts | 54 +- src/bundles/sound_matrix/functions.ts | 754 +++--- src/bundles/sound_matrix/index.ts | 50 +- src/bundles/sound_matrix/list.ts | 744 +++--- src/bundles/sound_matrix/types.ts | 12 +- src/bundles/stereo_sound/functions.ts | 2245 +++++++++--------- src/bundles/stereo_sound/index.ts | 232 +- src/bundles/stereo_sound/list.ts | 744 +++--- src/bundles/stereo_sound/riffwave.ts | 38 +- src/bundles/stereo_sound/types.ts | 44 +- src/tabs/CopyGc/index.tsx | 150 +- src/tabs/CopyGc/style.tsx | 26 +- src/tabs/Csg/canvas_holder.tsx | 232 +- src/tabs/Csg/hover_control_hint.tsx | 136 +- src/tabs/Csg/index.tsx | 90 +- src/tabs/Csg/types.ts | 48 +- src/tabs/Curve/3Dcurve_anim_canvas.tsx | 700 +++--- src/tabs/Curve/curve_canvas3d.tsx | 368 +-- src/tabs/Curve/index.tsx | 46 +- src/tabs/Game/constants.ts | 12 +- src/tabs/Game/index.tsx | 82 +- src/tabs/MarkSweep/index.tsx | 85 +- src/tabs/MarkSweep/style.tsx | 26 +- src/tabs/Pixnflix/index.tsx | 48 +- src/tabs/Rune/hollusion_canvas.tsx | 72 +- src/tabs/Sound/index.tsx | 150 +- src/tabs/SoundMatrix/index.tsx | 228 +- src/tabs/StereoSound/index.tsx | 144 +- src/tabs/common/animation_canvas.tsx | 638 +++-- src/tabs/common/multi_item_display.tsx | 156 +- src/tabs/common/webgl_canvas.tsx | 74 +- src/typings/anim_types.tsx | 40 +- src/typings/type_helpers.ts | 50 +- 63 files changed, 13701 insertions(+), 13614 deletions(-) diff --git a/src/bundles/copy_gc/index.ts b/src/bundles/copy_gc/index.ts index 832f2cd68..81b52e0fc 100644 --- a/src/bundles/copy_gc/index.ts +++ b/src/bundles/copy_gc/index.ts @@ -111,7 +111,7 @@ function newCommand( heap, description, firstDesc, - lastDesc + lastDesc, ): void { const newType = type; const newToSpace = toSpace; @@ -166,7 +166,7 @@ function newCopy(left, right, heap): void { heap, desc, 'index', - 'free' + 'free', ); } @@ -187,7 +187,7 @@ function endFlip(left, heap): void { heap, desc, 'free', - '' + '', ); updateFlip(); } @@ -215,7 +215,7 @@ function startFlip(toSpace, fromSpace, heap): void { heap, desc, '', - '' + '', ); updateFlip(); } @@ -236,7 +236,7 @@ function newPush(left, right, heap): void { heap, desc, 'last child address slot', - 'new child pushed' + 'new child pushed', ); } @@ -257,14 +257,14 @@ function newPop(res, left, right, heap): void { heap, desc, 'popped memory', - 'last child address slot' + 'last child address slot', ); } function doneShowRoot(heap): void { const toSpace = 0; const fromSpace = 0; - const desc = `All root nodes are copied`; + const desc = 'All root nodes are copied'; newCommand( 'Copied Roots', toSpace, @@ -276,7 +276,7 @@ function doneShowRoot(heap): void { heap, desc, '', - '' + '', ); } @@ -297,7 +297,7 @@ function showRoots(left, heap): void { heap, desc, 'roots', - '' + '', ); } @@ -318,7 +318,7 @@ function newAssign(res, left, heap): void { heap, desc, 'assigned memory', - '' + '', ); } @@ -339,7 +339,7 @@ function newNew(left, heap): void { heap, desc, 'new memory allocated', - '' + '', ); } @@ -390,7 +390,7 @@ function updateSlotSegment( tag: number, size: number, first: number, - last: number + last: number, ): void { if (tag >= 0) { TAG_SLOT = tag; diff --git a/src/bundles/copy_gc/types.ts b/src/bundles/copy_gc/types.ts index 32ae5b243..61caf5942 100644 --- a/src/bundles/copy_gc/types.ts +++ b/src/bundles/copy_gc/types.ts @@ -1,32 +1,32 @@ -export type Memory = number[]; -export type MemoryHeaps = Memory[]; -export type Tag = number; - -// command type - -export enum COMMAND { - FLIP = 'Flip', - PUSH = 'Push', - POP = 'Pop', - COPY = 'Copy', - ASSIGN = 'Assign', - NEW = 'New', - SCAN = 'Scan', - INIT = 'Initialize Memory', -} - -export type CommandHeapObject = { - type: String; - to: number; - from: number; - heap: number[]; - left: number; - right: number; - sizeLeft: number; - sizeRight: number; - desc: String; - scan: number; - leftDesc: String; - rightDesc: String; - free: number; -}; +export type Memory = number[]; +export type MemoryHeaps = Memory[]; +export type Tag = number; + +// command type + +export enum COMMAND { + FLIP = 'Flip', + PUSH = 'Push', + POP = 'Pop', + COPY = 'Copy', + ASSIGN = 'Assign', + NEW = 'New', + SCAN = 'Scan', + INIT = 'Initialize Memory', +} + +export type CommandHeapObject = { + type: String; + to: number; + from: number; + heap: number[]; + left: number; + right: number; + sizeLeft: number; + sizeRight: number; + desc: String; + scan: number; + leftDesc: String; + rightDesc: String; + free: number; +}; diff --git a/src/bundles/csg/constants.ts b/src/bundles/csg/constants.ts index d915c3d53..2d3352a42 100644 --- a/src/bundles/csg/constants.ts +++ b/src/bundles/csg/constants.ts @@ -1,26 +1,26 @@ -/* [Exports] */ -// Silver is in here to avoid circular dependencies -export const SILVER: string = '#AAAAAA'; -export const DEFAULT_COLOR: string = SILVER; - -// Values extracted from the styling of the frontend -export const SA_TAB_BUTTON_WIDTH: string = '40px'; -export const SA_TAB_ICON_SIZE: number = 20; - -export const BP_TOOLTIP_PADDING: string = '10px 12px'; -export const BP_TAB_BUTTON_MARGIN: string = '20px'; -export const BP_TAB_PANEL_MARGIN: string = '20px'; -export const BP_BORDER_RADIUS: string = '3px'; - -export const BP_TEXT_COLOR: string = '#F5F8FA'; -export const BP_TOOLTIP_BACKGROUND_COLOR: string = '#E1E8ED'; -export const BP_ICON_COLOR: string = '#A7B6C2'; -export const ACE_GUTTER_TEXT_COLOR: string = '#8091A0'; -export const ACE_GUTTER_BACKGROUND_COLOR: string = '#34495E'; -export const BP_TOOLTIP_TEXT_COLOR: string = '#394B59'; - -// Renderer grid constants -export const MAIN_TICKS = 1; -export const SUB_TICKS = MAIN_TICKS / 4; -export const GRID_PADDING = MAIN_TICKS; -export const ROUND_UP_INTERVAL = MAIN_TICKS; +/* [Exports] */ +// Silver is in here to avoid circular dependencies +export const SILVER: string = '#AAAAAA'; +export const DEFAULT_COLOR: string = SILVER; + +// Values extracted from the styling of the frontend +export const SA_TAB_BUTTON_WIDTH: string = '40px'; +export const SA_TAB_ICON_SIZE: number = 20; + +export const BP_TOOLTIP_PADDING: string = '10px 12px'; +export const BP_TAB_BUTTON_MARGIN: string = '20px'; +export const BP_TAB_PANEL_MARGIN: string = '20px'; +export const BP_BORDER_RADIUS: string = '3px'; + +export const BP_TEXT_COLOR: string = '#F5F8FA'; +export const BP_TOOLTIP_BACKGROUND_COLOR: string = '#E1E8ED'; +export const BP_ICON_COLOR: string = '#A7B6C2'; +export const ACE_GUTTER_TEXT_COLOR: string = '#8091A0'; +export const ACE_GUTTER_BACKGROUND_COLOR: string = '#34495E'; +export const BP_TOOLTIP_TEXT_COLOR: string = '#394B59'; + +// Renderer grid constants +export const MAIN_TICKS = 1; +export const SUB_TICKS = MAIN_TICKS / 4; +export const GRID_PADDING = MAIN_TICKS; +export const ROUND_UP_INTERVAL = MAIN_TICKS; diff --git a/src/bundles/csg/core.ts b/src/bundles/csg/core.ts index b4207796d..fad086102 100644 --- a/src/bundles/csg/core.ts +++ b/src/bundles/csg/core.ts @@ -1,19 +1,19 @@ -/* [Imports] */ -import { CsgModuleState, RenderGroupManager } from './utilities.js'; - -/* [Exports] */ -// After bundle initialises, tab will need to reinit on its end, as they run -// independently and are different versions of Core. Same reason why we need -// looseInstanceof() -export class Core { - private static moduleState: CsgModuleState | null = null; - - static initialize(csgModuleState: CsgModuleState): void { - Core.moduleState = csgModuleState; - } - - static getRenderGroupManager(): RenderGroupManager { - let moduleState: CsgModuleState = Core.moduleState as CsgModuleState; - return moduleState.renderGroupManager; - } -} +/* [Imports] */ +import { CsgModuleState, RenderGroupManager } from './utilities.js'; + +/* [Exports] */ +// After bundle initialises, tab will need to reinit on its end, as they run +// independently and are different versions of Core. Same reason why we need +// looseInstanceof() +export class Core { + private static moduleState: CsgModuleState | null = null; + + static initialize(csgModuleState: CsgModuleState): void { + Core.moduleState = csgModuleState; + } + + static getRenderGroupManager(): RenderGroupManager { + let moduleState: CsgModuleState = Core.moduleState as CsgModuleState; + return moduleState.renderGroupManager; + } +} diff --git a/src/bundles/csg/functions.ts b/src/bundles/csg/functions.ts index 4e7a570c4..eab3d0b08 100644 --- a/src/bundles/csg/functions.ts +++ b/src/bundles/csg/functions.ts @@ -1,825 +1,840 @@ -/** - * The module `csg` provides functions for drawing Constructive Solid Geometry (CSG) called `Shape`. - * - * A *Shape* is defined by its polygons and vertices. - * - * @module csg - * @author Liu Muchen - * @author Joel Leow - */ - -/* [Imports] */ -import { primitives } from '@jscad/modeling'; -import { colorize } from '@jscad/modeling/src/colors'; -import { - BoundingBox, - measureArea, - measureBoundingBox, - measureVolume, -} from '@jscad/modeling/src/measurements'; -import { - intersect as _intersect, - subtract as _subtract, - union as _union, -} from '@jscad/modeling/src/operations/booleans'; -import { extrudeLinear } from '@jscad/modeling/src/operations/extrusions'; -import { - align, - center, - mirror, - rotate as _rotate, - scale as _scale, - translate as _translate, -} from '@jscad/modeling/src/operations/transforms'; -import { DEFAULT_COLOR, SILVER } from './constants.js'; -import { Core } from './core.js'; -import { Color, CoordinatesXYZ, Solid } from './types'; -import { clamp, hexToColor, RenderGroup, Shape } from './utilities'; - -/* [Exports] */ - -// [Variables - Primitive shapes] - -/** - * Primitive Shape of a cube. - * - * @category Primitive - */ -export const cube: Shape = shapeSetOrigin( - new Shape(primitives.cube({ size: 1 })) -); - -/** - * Primitive Shape of a sphere. - * - * @category Primitive - */ -export const sphere: Shape = shapeSetOrigin( - new Shape(primitives.sphere({ radius: 0.5 })) -); - -/** - * Primitive Shape of a cylinder. - * - * @category Primitive - */ -export const cylinder: Shape = shapeSetOrigin( - new Shape(primitives.cylinder({ radius: 0.5, height: 1 })) -); - -/** - * Primitive Shape of a prism. - * - * @category Primitive - */ -export const prism: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.triangle())) -); - -/** - * Primitive Shape of an extruded star. - * - * @category Primitive - */ -export const star: Shape = shapeSetOrigin( - new Shape(extrudeLinear({ height: 1 }, primitives.star({ outerRadius: 0.5 }))) -); - -//TODO -let small = 10 ** -30; - -/** - * Primitive Shape of a square pyramid. - * - * @category Primitive - */ -export const pyramid: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [small, small], - segments: 4, - }) - ) -); - -/** - * Primitive Shape of a cone. - * - * @category Primitive - */ -export const cone: Shape = shapeSetOrigin( - new Shape( - primitives.cylinderElliptic({ - height: 1, - startRadius: [0.5, 0.5], - endRadius: [small, small], - }) - ) -); - -/** - * Primitive Shape of a torus. - * - * @category Primitive - */ -export const torus: Shape = shapeSetOrigin( - new Shape(primitives.torus({ innerRadius: 0.125, outerRadius: 0.375 })) -); - -/** - * Primitive Shape of a rounded cube. - * - * @category Primitive - */ -export const rounded_cube: Shape = shapeSetOrigin( - new Shape(primitives.roundedCuboid({ size: [1, 1, 1] })) -); - -/** - * Primitive Shape of a rounded cylinder. - * - * @category Primitive - */ -export const rounded_cylinder: Shape = shapeSetOrigin( - new Shape(primitives.roundedCylinder({ height: 1, radius: 0.5 })) -); - -/** - * Primitive Shape of a geodesic sphere. - * - * @category Primitive - */ -export const geodesic_sphere: Shape = shapeSetOrigin( - new Shape(primitives.geodesicSphere({ radius: 0.5 })) -); - -// [Variables - Colours] - -/** - * A hex colour code for black (#000000). - * - * @category Colour - */ -export const black: string = '#000000'; - -/** - * A hex colour code for dark blue (#0000AA). - * - * @category Colour - */ -export const navy: string = '#0000AA'; - -/** - * A hex colour code for green (#00AA00). - * - * @category Colour - */ -export const green: string = '#00AA00'; - -/** - * A hex colour code for dark cyan (#00AAAA). - * - * @category Colour - */ -export const teal: string = '#00AAAA'; - -/** - * A hex colour code for dark red (#AA0000). - * - * @category Colour - */ -export const crimson: string = '#AA0000'; - -/** - * A hex colour code for purple (#AA00AA). - * - * @category Colour - */ -export const purple: string = '#AA00AA'; - -/** - * A hex colour code for orange (#FFAA00). - * - * @category Colour - */ -export const orange: string = '#FFAA00'; - -/** - * A hex colour code for light grey (#AAAAAA). This is the default colour used - * when storing a Shape. - * - * @category Colour - */ -export const silver: string = SILVER; - -/** - * A hex colour code for dark grey (#555555). - * - * @category Colour - */ -export const gray: string = '#555555'; - -/** - * A hex colour code for blue (#5555FF). - * - * @category Colour - */ -export const blue: string = '#5555FF'; - -/** - * A hex colour code for light green (#55FF55). - * - * @category Colour - */ -export const lime: string = '#55FF55'; - -/** - * A hex colour code for cyan (#55FFFF). - * - * @category Colour - */ -export const cyan: string = '#55FFFF'; - -/** - * A hex colour code for light red (#FF5555). - * - * @category Colour - */ -export const rose: string = '#FF5555'; - -/** - * A hex colour code for pink (#FF55FF). - * - * @category Colour - */ -export const pink: string = '#FF55FF'; - -/** - * A hex colour code for yellow (#FFFF55). - * - * @category Colour - */ -export const yellow: string = '#FFFF55'; - -/** - * A hex colour code for white (#FFFFFF). - * - * @category Colour - */ -export const white: string = '#FFFFFF'; - -// [Functions] - -/** - * Union of the two provided shapes to produce a new shape. - * - * @param {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting unioned shape - */ -export function union(a: Shape, b: Shape): Shape { - let newSolid: Solid = _union(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Subtraction of the second shape from the first shape to produce a new shape. - * - * @param {Shape} a - The shape to be subtracted from - * @param {Shape} b - The shape to remove from the first shape - * @returns {Shape} The resulting subtracted shape - */ -export function subtract(a: Shape, b: Shape): Shape { - let newSolid: Solid = _subtract(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Intersection of the two shape to produce a new shape. - * - * @param {Shape} a - The first shape - * @param {Shape} b - The second shape - * @returns {Shape} The resulting intersection shape - */ -export function intersect(a: Shape, b: Shape): Shape { - let newSolid: Solid = _intersect(a.solid, b.solid); - return new Shape(newSolid); -} - -/** - * Scales the shape in the x, y and z direction with the specified factor, - * ranging from 0 to infinity. - * For example scaling the shape by 1 in x, y and z direction results in - * the original shape. - * - * @param {Shape} shape - The shape to be scaled - * @param {number} x - Scaling in the x direction - * @param {number} y - Scaling in the y direction - * @param {number} z - Scaling in the z direction - * @returns {Shape} Resulting Shape - */ -export function scale(shape: Shape, x: number, y: number, z: number): Shape { - let newSolid: Solid = _scale([x, y, z], shape.solid); - return new Shape(newSolid); -} - -/** - * Scales the shape in the x direction with the specified factor, - * ranging from 0 to infinity. - * For example scaling the shape by 1 in x direction results in the - * original shape. - * - * @param {Shape} shape - The shape to be scaled - * @param {number} x - Scaling in the x direction - * @returns {Shape} Resulting Shape - */ -export function scale_x(shape: Shape, x: number): Shape { - return scale(shape, x, 1, 1); -} - -/** - * Scales the shape in the y direction with the specified factor, - * ranging from 0 to infinity. - * For example scaling the shape by 1 in y direction results in the - * original shape. - * - * @param {Shape} shape - The shape to be scaled - * @param {number} y - Scaling in the y direction - * @returns {Shape} Resulting Shape - */ -export function scale_y(shape: Shape, y: number): Shape { - return scale(shape, 1, y, 1); -} - -/** - * Scales the shape in the z direction with the specified factor, - * ranging from 0 to infinity. - * For example scaling the shape by 1 in z direction results in the - * original shape. - * - * @param {Shape} shape - The shape to be scaled - * @param {number} z - Scaling in the z direction - * @returns {Shape} Resulting Shape - */ -export function scale_z(shape: Shape, z: number): Shape { - return scale(shape, 1, 1, z); -} - -/** - * Returns a lambda function that contains the center of the given shape in the - * x, y and z direction. Providing 'x', 'y', 'z' as input would return x, y and - * z coordinates of shape's center - * - * For example - * ```` - * const a = shape_center(sphere); - * a('x'); // Returns the x coordinate of the shape's center - * ```` - * - * @param {Shape} shape - The scale to be measured - * @returns {(String) => number} A lambda function providing the shape's center - * coordinates - */ -export function shape_center(shape: Shape): (axis: String) => number { - let bounds: BoundingBox = measureBoundingBox(shape.solid); - let centerCoords: CoordinatesXYZ = [ - bounds[0][0] + (bounds[1][0] - bounds[0][0]) / 2, - bounds[0][1] + (bounds[1][1] - bounds[0][1]) / 2, - bounds[0][2] + (bounds[1][2] - bounds[0][2]) / 2, - ]; - return (axis: String): number => { - let i: number = axis === 'x' ? 0 : axis === 'y' ? 1 : axis === 'z' ? 2 : -1; - if (i === -1) { - throw Error(`shape_center's returned function expects a proper axis.`); - } else { - return centerCoords[i]; - } - }; -} - -/** - * Set the center of the shape with the provided x, y and z coordinates. - * - * @param {Shape} shape - The scale to have the center set - * @param {nunber} x - The center with the x coordinate - * @param {nunber} y - The center with the y coordinate - * @param {nunber} z - The center with the z coordinate - * @returns {Shape} The shape with the new center - */ -export function shape_set_center( - shape: Shape, - x: number, - y: number, - z: number -): Shape { - let newSolid: Solid = center({ relativeTo: [x, y, z] }, shape.solid); - return new Shape(newSolid); -} - -/** - * Measure the area of the provided shape. - * - * @param {Shape} shape - The shape to measure the area from - * @returns {number} The area of the shape - */ -export function area(shape: Shape): number { - return measureArea(shape.solid); -} - -/** - * Measure the volume of the provided shape. - * - * @param {Shape} shape - The shape to measure the volume from - * @returns {number} The volume of the shape - */ -export function volume(shape: Shape): number { - return measureVolume(shape.solid); -} - -//TODO -/** - * Mirror / Flip the provided shape by the plane with normal direction vector - * given by the x, y and z components. - * - * @param {Shape} shape - The shape to mirror / flip - * @param {number} x - The x coordinate of the direction vector - * @param {number} y - The y coordinate of the direction vector - * @param {number} z - The z coordinate of the direction vector - * @returns {Shape} The mirrored / flipped shape - */ -function shape_mirror(shape: Shape, x: number, y: number, z: number) { - let newSolid: Solid = mirror({ normal: [x, y, z] }, shape.solid); - return new Shape(newSolid); -} - -/** - * Mirror / Flip the provided shape in the x direction. - * - * @param {Shape} shape - The shape to mirror / flip - * @returns {Shape} The mirrored / flipped shape - */ -export function flip_x(shape: Shape): Shape { - return shape_mirror(shape, 1, 0, 0); -} - -/** - * Mirror / Flip the provided shape in the y direction. - * - * @param {Shape} shape - The shape to mirror / flip - * @returns {Shape} The mirrored / flipped shape - */ -export function flip_y(shape: Shape): Shape { - return shape_mirror(shape, 0, 1, 0); -} - -/** - * Mirror / Flip the provided shape in the z direction. - * - * @param {Shape} shape - The shape to mirror / flip - * @returns {Shape} The mirrored / flipped shape - */ -export function flip_z(shape: Shape): Shape { - return shape_mirror(shape, 0, 0, 1); -} - -/** - * Translate / Move the shape by the provided x, y and z units from negative - * infinity to infinity. - * - * @param {Shape} shape - * @param {number} x - The number to shift the shape in the x direction - * @param {number} y - The number to shift the shape in the y direction - * @param {number} z - The number to shift the shape in the z direction - * @returns {Shape} The translated shape - */ -export function translate( - shape: Shape, - x: number, - y: number, - z: number -): Shape { - let newSolid: Solid = _translate([x, y, z], shape.solid); - return new Shape(newSolid); -} - -/** - * Translate / Move the shape by the provided x units from negative infinity - * to infinity. - * - * @param {Shape} shape - * @param {number} x - The number to shift the shape in the x direction - * @returns {Shape} The translated shape - */ -export function translate_x(shape: Shape, x: number): Shape { - return translate(shape, x, 0, 0); -} - -/** - * Translate / Move the shape by the provided y units from negative infinity - * to infinity. - * - * @param {Shape} shape - * @param {number} y - The number to shift the shape in the y direction - * @returns {Shape} The translated shape - */ -export function translate_y(shape: Shape, y: number): Shape { - return translate(shape, 0, y, 0); -} - -/** - * Translate / Move the shape by the provided z units from negative infinity - * to infinity. - * - * @param {Shape} shape - * @param {number} z - The number to shift the shape in the z direction - * @returns {Shape} The translated shape - */ -export function translate_z(shape: Shape, z: number): Shape { - return translate(shape, 0, 0, z); -} - -/** - * Places the second shape `b` beside the first shape `a` in the positive x direction, - * centering the `b`'s y and z on the `a`'s y and z center. - * - * @param {Shape} a - The shape to be placed beside with - * @param {Shape} b - The shape placed beside - * @returns {Shape} The final shape - */ -export function beside_x(a: Shape, b: Shape): Shape { - let aBounds: BoundingBox = measureBoundingBox(a.solid); - let newX: number = aBounds[1][0]; - let newY: number = aBounds[0][1] + (aBounds[1][1] - aBounds[0][1]) / 2; - let newZ: number = aBounds[0][2] + (aBounds[1][2] - aBounds[0][2]) / 2; - let newSolid: Solid = _union( - a.solid, // @ts-ignore - align( - { - modes: ['min', 'center', 'center'], - relativeTo: [newX, newY, newZ], - }, - b.solid - ) - ); - return new Shape(newSolid); -} - -/** - * Places the second shape `b` beside the first shape `a` in the positive y direction, - * centering the `b`'s x and z on the `a`'s x and z center. - * - * @param {Shape} a - The shape to be placed beside with - * @param {Shape} b - The shape placed beside - * @returns {Shape} The final shape - */ -export function beside_y(a: Shape, b: Shape): Shape { - let aBounds: BoundingBox = measureBoundingBox(a.solid); - let newX: number = aBounds[0][0] + (aBounds[1][0] - aBounds[0][0]) / 2; - let newY: number = aBounds[1][1]; - let newZ: number = aBounds[0][2] + (aBounds[1][2] - aBounds[0][2]) / 2; - let newSolid: Solid = _union( - a.solid, // @ts-ignore - align( - { - modes: ['center', 'min', 'center'], - relativeTo: [newX, newY, newZ], - }, - b.solid - ) - ); - return new Shape(newSolid); -} - -/** - * Places the second shape `b` beside the first shape `a` in the positive z direction, - * centering the `b`'s x and y on the `a`'s x and y center. - * - * @param {Shape} a - The shape to be placed beside with - * @param {Shape} b - The shape placed beside - * @returns {Shape} The final shape - */ -export function beside_z(a: Shape, b: Shape): Shape { - let aBounds: BoundingBox = measureBoundingBox(a.solid); - let newX: number = aBounds[0][0] + (aBounds[1][0] - aBounds[0][0]) / 2; - let newY: number = aBounds[0][1] + (aBounds[1][1] - aBounds[0][1]) / 2; - let newZ: number = aBounds[1][2]; - let newSolid: Solid = _union( - a.solid, // @ts-ignore - align( - { - modes: ['center', 'center', 'min'], - relativeTo: [newX, newY, newZ], - }, - b.solid - ) - ); - return new Shape(newSolid); -} - -/** - * Returns a lambda function that contains the coordinates of the bounding box. - * Provided with the axis 'x', 'y' or 'z' and value 'min' for minimum and 'max' - * for maximum, it returns the coordinates of the bounding box. - * - * For example - * ```` - * const a = bounding_box(sphere); - * a('x', 'min'); // Returns the maximum x coordinate of the bounding box - * ```` - * - * @param {Shape} shape - The scale to be measured - * @returns {(String, String) => number} A lambda function providing the - * shape's bounding box coordinates - */ - -export function bounding_box( - shape: Shape -): (axis: String, min: String) => number { - let bounds: BoundingBox = measureBoundingBox(shape.solid); - return (axis: String, min: String): number => { - let i: number = axis === 'x' ? 0 : axis === 'y' ? 1 : axis === 'z' ? 2 : -1; - let j: number = min === 'min' ? 0 : min === 'max' ? 1 : -1; - if (i === -1 || j === -1) { - throw Error( - `bounding_box returned function expects a proper axis and min String.` - ); - } else { - return bounds[j][i]; - } - }; -} - -/** - * Rotate the shape by the provided angles in the x, y and z direction. - * Angles provided are in the form of radians (i.e. 2π represent 360 - * degrees) - * - * @param {Shape} shape - The shape to be rotated - * @param {number} x - Angle of rotation in the x direction - * @param {number} y - Angle of rotation in the y direction - * @param {number} z - Angle of rotation in the z direction - * @returns {Shape} The rotated shape - */ -export function rotate(shape: Shape, x: number, y: number, z: number): Shape { - let newSolid: Solid = _rotate([x, y, z], shape.solid); - return new Shape(newSolid); -} - -/** - * Rotate the shape by the provided angles in the x direction. Angles - * provided are in the form of radians (i.e. 2π represent 360 degrees) - * - * @param {Shape} shape - The shape to be rotated - * @param {number} x - Angle of rotation in the x direction - * @returns {Shape} The rotated shape - */ -export function rotate_x(shape: Shape, x: number): Shape { - return rotate(shape, x, 0, 0); -} - -/** - * Rotate the shape by the provided angles in the y direction. Angles - * provided are in the form of radians (i.e. 2π represent 360 degrees) - * - * @param {Shape} shape - The shape to be rotated - * @param {number} y - Angle of rotation in the y direction - * @returns {Shape} The rotated shape - */ -export function rotate_y(shape: Shape, y: number): Shape { - return rotate(shape, 0, y, 0); -} - -/** - * Rotate the shape by the provided angles in the z direction. Angles - * provided are in the form of radians (i.e. 2π represent 360 degrees) - * - * @param {Shape} shape - The shape to be rotated - * @param {number} z - Angle of rotation in the z direction - * @returns {Shape} The rotated shape - */ -export function rotate_z(shape: Shape, z: number): Shape { - return rotate(shape, 0, 0, z); -} - -//TODO -/** - * Center the provided shape with the middle base of the shape at (0, 0, 0). - * - * @param {Shape} shape - The shape to be centered - * @returns {Shape} The shape that is centered - */ -function shapeSetOrigin(shape: Shape) { - let newSolid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); - return new Shape(newSolid); -} - -/** - * Checks if the specified argument is a Shape. - * - * @param {unknown} argument - The value to check. - * @returns {boolean} Whether the argument is a Shape. - */ -export function is_shape(argument: unknown): boolean { - return argument instanceof Shape; -} - -/** - * Creates a clone of the specified Shape. - * - * @param {Shape} shape - The Shape to be cloned. - * @returns {Shape} The cloned Shape. - */ -export function clone(shape: Shape): Shape { - return shape.clone(); -} - -/** - * Stores a clone of the specified Shape for later rendering. Its colour - * defaults to the module's provided silver colour variable. - * - * @param {Shape} shape - The Shape to be stored. - */ -export function store(shape: Shape): void { - store_as_color(shape, DEFAULT_COLOR); -} - -/** - * Colours a clone of the specified Shape using the specified hex colour code, - * then stores it for later rendering. You may use one of the colour variables - * provided by the module, or you may specify your own custom colour code. - * - * Colour codes must be of the form "#XXXXXX" or "XXXXXX", where each X - * represents a non-case sensitive hexadecimal number. Invalid colour codes - * default to black. - * - * @param {Shape} shape - The Shape to be coloured and stored. - * @param {string} hex - The colour code to use. - */ -export function store_as_color(shape: Shape, hex: string): void { - let color: Color = hexToColor(hex); - let coloredSolid: Solid = colorize(color, shape.solid); - Core.getRenderGroupManager().storeShape(new Shape(coloredSolid)); -} - -/** - * Colours a clone of the specified Shape using the specified RGB values, then - * stores it for later rendering. - * - * RGB values are clamped between 0 and 1. - * - * @param {Shape} shape - The Shape to be coloured and stored. - * @param {number} redComponent - The colour's red component. - * @param {number} greenComponent - The colour's green component. - * @param {number} blueComponent - The colour's blue component. - */ -export function store_as_rgb( - shape: Shape, - redComponent: number, - greenComponent: number, - blueComponent: number -): void { - redComponent = clamp(redComponent, 0, 1); - greenComponent = clamp(greenComponent, 0, 1); - blueComponent = clamp(blueComponent, 0, 1); - - let coloredSolid: Solid = colorize( - [redComponent, greenComponent, blueComponent], - shape.solid - ); - Core.getRenderGroupManager().storeShape(new Shape(coloredSolid)); -} - -/** - * Renders using any Shapes stored thus far, along with a grid and axis. The - * Shapes will then not be included in any subsequent renders. - */ -export function render_grid_axis(): RenderGroup { - // Render group is returned for REPL text only; do not document - return Core.getRenderGroupManager().nextRenderGroup(true, true); -} - -/** - * Renders using any Shapes stored thus far, along with a grid. The Shapes will - * then not be included in any subsequent renders. - */ -export function render_grid(): RenderGroup { - return Core.getRenderGroupManager().nextRenderGroup(true); -} - -/** - * Renders using any Shapes stored thus far, along with an axis. The Shapes will - * then not be included in any subsequent renders. - */ -export function render_axis(): RenderGroup { - return Core.getRenderGroupManager().nextRenderGroup(undefined, true); -} - -/** - * Renders using any Shapes stored thus far. The Shapes will then not be - * included in any subsequent renders. - */ -export function render(): RenderGroup { - return Core.getRenderGroupManager().nextRenderGroup(); -} +/** + * The module `csg` provides functions for drawing Constructive Solid Geometry (CSG) called `Shape`. + * + * A *Shape* is defined by its polygons and vertices. + * + * @module csg + * @author Liu Muchen + * @author Joel Leow + */ + +/* [Imports] */ +import { primitives } from '@jscad/modeling'; +import { colorize } from '@jscad/modeling/src/colors'; +import { + BoundingBox, + measureArea, + measureBoundingBox, + measureVolume, +} from '@jscad/modeling/src/measurements'; +import { + intersect as _intersect, + subtract as _subtract, + union as _union, +} from '@jscad/modeling/src/operations/booleans'; +import { extrudeLinear } from '@jscad/modeling/src/operations/extrusions'; +import { + align, + center, + mirror, + rotate as _rotate, + scale as _scale, + translate as _translate, +} from '@jscad/modeling/src/operations/transforms'; +import { DEFAULT_COLOR, SILVER } from './constants.js'; +import { Core } from './core.js'; +import { Color, CoordinatesXYZ, Solid } from './types'; +import { clamp, hexToColor, RenderGroup, Shape } from './utilities'; + +/* [Exports] */ + +// [Variables - Primitive shapes] + +/** + * Primitive Shape of a cube. + * + * @category Primitive + */ +export const cube: Shape = shapeSetOrigin( + new Shape(primitives.cube({ size: 1 })), +); + +/** + * Primitive Shape of a sphere. + * + * @category Primitive + */ +export const sphere: Shape = shapeSetOrigin( + new Shape(primitives.sphere({ radius: 0.5 })), +); + +/** + * Primitive Shape of a cylinder. + * + * @category Primitive + */ +export const cylinder: Shape = shapeSetOrigin( + new Shape(primitives.cylinder({ + radius: 0.5, + height: 1, + })), +); + +/** + * Primitive Shape of a prism. + * + * @category Primitive + */ +export const prism: Shape = shapeSetOrigin( + new Shape(extrudeLinear({ height: 1 }, primitives.triangle())), +); + +/** + * Primitive Shape of an extruded star. + * + * @category Primitive + */ +export const star: Shape = shapeSetOrigin( + new Shape(extrudeLinear({ height: 1 }, primitives.star({ outerRadius: 0.5 }))), +); + +//TODO +let small = 10 ** -30; + +/** + * Primitive Shape of a square pyramid. + * + * @category Primitive + */ +export const pyramid: Shape = shapeSetOrigin( + new Shape( + primitives.cylinderElliptic({ + height: 1, + startRadius: [0.5, 0.5], + endRadius: [small, small], + segments: 4, + }), + ), +); + +/** + * Primitive Shape of a cone. + * + * @category Primitive + */ +export const cone: Shape = shapeSetOrigin( + new Shape( + primitives.cylinderElliptic({ + height: 1, + startRadius: [0.5, 0.5], + endRadius: [small, small], + }), + ), +); + +/** + * Primitive Shape of a torus. + * + * @category Primitive + */ +export const torus: Shape = shapeSetOrigin( + new Shape(primitives.torus({ + innerRadius: 0.125, + outerRadius: 0.375, + })), +); + +/** + * Primitive Shape of a rounded cube. + * + * @category Primitive + */ +export const rounded_cube: Shape = shapeSetOrigin( + new Shape(primitives.roundedCuboid({ size: [1, 1, 1] })), +); + +/** + * Primitive Shape of a rounded cylinder. + * + * @category Primitive + */ +export const rounded_cylinder: Shape = shapeSetOrigin( + new Shape(primitives.roundedCylinder({ + height: 1, + radius: 0.5, + })), +); + +/** + * Primitive Shape of a geodesic sphere. + * + * @category Primitive + */ +export const geodesic_sphere: Shape = shapeSetOrigin( + new Shape(primitives.geodesicSphere({ radius: 0.5 })), +); + +// [Variables - Colours] + +/** + * A hex colour code for black (#000000). + * + * @category Colour + */ +export const black: string = '#000000'; + +/** + * A hex colour code for dark blue (#0000AA). + * + * @category Colour + */ +export const navy: string = '#0000AA'; + +/** + * A hex colour code for green (#00AA00). + * + * @category Colour + */ +export const green: string = '#00AA00'; + +/** + * A hex colour code for dark cyan (#00AAAA). + * + * @category Colour + */ +export const teal: string = '#00AAAA'; + +/** + * A hex colour code for dark red (#AA0000). + * + * @category Colour + */ +export const crimson: string = '#AA0000'; + +/** + * A hex colour code for purple (#AA00AA). + * + * @category Colour + */ +export const purple: string = '#AA00AA'; + +/** + * A hex colour code for orange (#FFAA00). + * + * @category Colour + */ +export const orange: string = '#FFAA00'; + +/** + * A hex colour code for light grey (#AAAAAA). This is the default colour used + * when storing a Shape. + * + * @category Colour + */ +export const silver: string = SILVER; + +/** + * A hex colour code for dark grey (#555555). + * + * @category Colour + */ +export const gray: string = '#555555'; + +/** + * A hex colour code for blue (#5555FF). + * + * @category Colour + */ +export const blue: string = '#5555FF'; + +/** + * A hex colour code for light green (#55FF55). + * + * @category Colour + */ +export const lime: string = '#55FF55'; + +/** + * A hex colour code for cyan (#55FFFF). + * + * @category Colour + */ +export const cyan: string = '#55FFFF'; + +/** + * A hex colour code for light red (#FF5555). + * + * @category Colour + */ +export const rose: string = '#FF5555'; + +/** + * A hex colour code for pink (#FF55FF). + * + * @category Colour + */ +export const pink: string = '#FF55FF'; + +/** + * A hex colour code for yellow (#FFFF55). + * + * @category Colour + */ +export const yellow: string = '#FFFF55'; + +/** + * A hex colour code for white (#FFFFFF). + * + * @category Colour + */ +export const white: string = '#FFFFFF'; + +// [Functions] + +/** + * Union of the two provided shapes to produce a new shape. + * + * @param {Shape} a - The first shape + * @param {Shape} b - The second shape + * @returns {Shape} The resulting unioned shape + */ +export function union(a: Shape, b: Shape): Shape { + let newSolid: Solid = _union(a.solid, b.solid); + return new Shape(newSolid); +} + +/** + * Subtraction of the second shape from the first shape to produce a new shape. + * + * @param {Shape} a - The shape to be subtracted from + * @param {Shape} b - The shape to remove from the first shape + * @returns {Shape} The resulting subtracted shape + */ +export function subtract(a: Shape, b: Shape): Shape { + let newSolid: Solid = _subtract(a.solid, b.solid); + return new Shape(newSolid); +} + +/** + * Intersection of the two shape to produce a new shape. + * + * @param {Shape} a - The first shape + * @param {Shape} b - The second shape + * @returns {Shape} The resulting intersection shape + */ +export function intersect(a: Shape, b: Shape): Shape { + let newSolid: Solid = _intersect(a.solid, b.solid); + return new Shape(newSolid); +} + +/** + * Scales the shape in the x, y and z direction with the specified factor, + * ranging from 0 to infinity. + * For example scaling the shape by 1 in x, y and z direction results in + * the original shape. + * + * @param {Shape} shape - The shape to be scaled + * @param {number} x - Scaling in the x direction + * @param {number} y - Scaling in the y direction + * @param {number} z - Scaling in the z direction + * @returns {Shape} Resulting Shape + */ +export function scale(shape: Shape, x: number, y: number, z: number): Shape { + let newSolid: Solid = _scale([x, y, z], shape.solid); + return new Shape(newSolid); +} + +/** + * Scales the shape in the x direction with the specified factor, + * ranging from 0 to infinity. + * For example scaling the shape by 1 in x direction results in the + * original shape. + * + * @param {Shape} shape - The shape to be scaled + * @param {number} x - Scaling in the x direction + * @returns {Shape} Resulting Shape + */ +export function scale_x(shape: Shape, x: number): Shape { + return scale(shape, x, 1, 1); +} + +/** + * Scales the shape in the y direction with the specified factor, + * ranging from 0 to infinity. + * For example scaling the shape by 1 in y direction results in the + * original shape. + * + * @param {Shape} shape - The shape to be scaled + * @param {number} y - Scaling in the y direction + * @returns {Shape} Resulting Shape + */ +export function scale_y(shape: Shape, y: number): Shape { + return scale(shape, 1, y, 1); +} + +/** + * Scales the shape in the z direction with the specified factor, + * ranging from 0 to infinity. + * For example scaling the shape by 1 in z direction results in the + * original shape. + * + * @param {Shape} shape - The shape to be scaled + * @param {number} z - Scaling in the z direction + * @returns {Shape} Resulting Shape + */ +export function scale_z(shape: Shape, z: number): Shape { + return scale(shape, 1, 1, z); +} + +/** + * Returns a lambda function that contains the center of the given shape in the + * x, y and z direction. Providing 'x', 'y', 'z' as input would return x, y and + * z coordinates of shape's center + * + * For example + * ```` + * const a = shape_center(sphere); + * a('x'); // Returns the x coordinate of the shape's center + * ```` + * + * @param {Shape} shape - The scale to be measured + * @returns {(String) => number} A lambda function providing the shape's center + * coordinates + */ +export function shape_center(shape: Shape): (axis: String) => number { + let bounds: BoundingBox = measureBoundingBox(shape.solid); + let centerCoords: CoordinatesXYZ = [ + bounds[0][0] + (bounds[1][0] - bounds[0][0]) / 2, + bounds[0][1] + (bounds[1][1] - bounds[0][1]) / 2, + bounds[0][2] + (bounds[1][2] - bounds[0][2]) / 2, + ]; + return (axis: String): number => { + let i: number = axis === 'x' ? 0 : axis === 'y' ? 1 : axis === 'z' ? 2 : -1; + if (i === -1) { + throw Error('shape_center\'s returned function expects a proper axis.'); + } else { + return centerCoords[i]; + } + }; +} + +/** + * Set the center of the shape with the provided x, y and z coordinates. + * + * @param {Shape} shape - The scale to have the center set + * @param {nunber} x - The center with the x coordinate + * @param {nunber} y - The center with the y coordinate + * @param {nunber} z - The center with the z coordinate + * @returns {Shape} The shape with the new center + */ +export function shape_set_center( + shape: Shape, + x: number, + y: number, + z: number, +): Shape { + let newSolid: Solid = center({ relativeTo: [x, y, z] }, shape.solid); + return new Shape(newSolid); +} + +/** + * Measure the area of the provided shape. + * + * @param {Shape} shape - The shape to measure the area from + * @returns {number} The area of the shape + */ +export function area(shape: Shape): number { + return measureArea(shape.solid); +} + +/** + * Measure the volume of the provided shape. + * + * @param {Shape} shape - The shape to measure the volume from + * @returns {number} The volume of the shape + */ +export function volume(shape: Shape): number { + return measureVolume(shape.solid); +} + +//TODO +/** + * Mirror / Flip the provided shape by the plane with normal direction vector + * given by the x, y and z components. + * + * @param {Shape} shape - The shape to mirror / flip + * @param {number} x - The x coordinate of the direction vector + * @param {number} y - The y coordinate of the direction vector + * @param {number} z - The z coordinate of the direction vector + * @returns {Shape} The mirrored / flipped shape + */ +function shape_mirror(shape: Shape, x: number, y: number, z: number) { + let newSolid: Solid = mirror({ normal: [x, y, z] }, shape.solid); + return new Shape(newSolid); +} + +/** + * Mirror / Flip the provided shape in the x direction. + * + * @param {Shape} shape - The shape to mirror / flip + * @returns {Shape} The mirrored / flipped shape + */ +export function flip_x(shape: Shape): Shape { + return shape_mirror(shape, 1, 0, 0); +} + +/** + * Mirror / Flip the provided shape in the y direction. + * + * @param {Shape} shape - The shape to mirror / flip + * @returns {Shape} The mirrored / flipped shape + */ +export function flip_y(shape: Shape): Shape { + return shape_mirror(shape, 0, 1, 0); +} + +/** + * Mirror / Flip the provided shape in the z direction. + * + * @param {Shape} shape - The shape to mirror / flip + * @returns {Shape} The mirrored / flipped shape + */ +export function flip_z(shape: Shape): Shape { + return shape_mirror(shape, 0, 0, 1); +} + +/** + * Translate / Move the shape by the provided x, y and z units from negative + * infinity to infinity. + * + * @param {Shape} shape + * @param {number} x - The number to shift the shape in the x direction + * @param {number} y - The number to shift the shape in the y direction + * @param {number} z - The number to shift the shape in the z direction + * @returns {Shape} The translated shape + */ +export function translate( + shape: Shape, + x: number, + y: number, + z: number, +): Shape { + let newSolid: Solid = _translate([x, y, z], shape.solid); + return new Shape(newSolid); +} + +/** + * Translate / Move the shape by the provided x units from negative infinity + * to infinity. + * + * @param {Shape} shape + * @param {number} x - The number to shift the shape in the x direction + * @returns {Shape} The translated shape + */ +export function translate_x(shape: Shape, x: number): Shape { + return translate(shape, x, 0, 0); +} + +/** + * Translate / Move the shape by the provided y units from negative infinity + * to infinity. + * + * @param {Shape} shape + * @param {number} y - The number to shift the shape in the y direction + * @returns {Shape} The translated shape + */ +export function translate_y(shape: Shape, y: number): Shape { + return translate(shape, 0, y, 0); +} + +/** + * Translate / Move the shape by the provided z units from negative infinity + * to infinity. + * + * @param {Shape} shape + * @param {number} z - The number to shift the shape in the z direction + * @returns {Shape} The translated shape + */ +export function translate_z(shape: Shape, z: number): Shape { + return translate(shape, 0, 0, z); +} + +/** + * Places the second shape `b` beside the first shape `a` in the positive x direction, + * centering the `b`'s y and z on the `a`'s y and z center. + * + * @param {Shape} a - The shape to be placed beside with + * @param {Shape} b - The shape placed beside + * @returns {Shape} The final shape + */ +export function beside_x(a: Shape, b: Shape): Shape { + let aBounds: BoundingBox = measureBoundingBox(a.solid); + let newX: number = aBounds[1][0]; + let newY: number = aBounds[0][1] + (aBounds[1][1] - aBounds[0][1]) / 2; + let newZ: number = aBounds[0][2] + (aBounds[1][2] - aBounds[0][2]) / 2; + let newSolid: Solid = _union( + a.solid, // @ts-ignore + align( + { + modes: ['min', 'center', 'center'], + relativeTo: [newX, newY, newZ], + }, + b.solid, + ), + ); + return new Shape(newSolid); +} + +/** + * Places the second shape `b` beside the first shape `a` in the positive y direction, + * centering the `b`'s x and z on the `a`'s x and z center. + * + * @param {Shape} a - The shape to be placed beside with + * @param {Shape} b - The shape placed beside + * @returns {Shape} The final shape + */ +export function beside_y(a: Shape, b: Shape): Shape { + let aBounds: BoundingBox = measureBoundingBox(a.solid); + let newX: number = aBounds[0][0] + (aBounds[1][0] - aBounds[0][0]) / 2; + let newY: number = aBounds[1][1]; + let newZ: number = aBounds[0][2] + (aBounds[1][2] - aBounds[0][2]) / 2; + let newSolid: Solid = _union( + a.solid, // @ts-ignore + align( + { + modes: ['center', 'min', 'center'], + relativeTo: [newX, newY, newZ], + }, + b.solid, + ), + ); + return new Shape(newSolid); +} + +/** + * Places the second shape `b` beside the first shape `a` in the positive z direction, + * centering the `b`'s x and y on the `a`'s x and y center. + * + * @param {Shape} a - The shape to be placed beside with + * @param {Shape} b - The shape placed beside + * @returns {Shape} The final shape + */ +export function beside_z(a: Shape, b: Shape): Shape { + let aBounds: BoundingBox = measureBoundingBox(a.solid); + let newX: number = aBounds[0][0] + (aBounds[1][0] - aBounds[0][0]) / 2; + let newY: number = aBounds[0][1] + (aBounds[1][1] - aBounds[0][1]) / 2; + let newZ: number = aBounds[1][2]; + let newSolid: Solid = _union( + a.solid, // @ts-ignore + align( + { + modes: ['center', 'center', 'min'], + relativeTo: [newX, newY, newZ], + }, + b.solid, + ), + ); + return new Shape(newSolid); +} + +/** + * Returns a lambda function that contains the coordinates of the bounding box. + * Provided with the axis 'x', 'y' or 'z' and value 'min' for minimum and 'max' + * for maximum, it returns the coordinates of the bounding box. + * + * For example + * ```` + * const a = bounding_box(sphere); + * a('x', 'min'); // Returns the maximum x coordinate of the bounding box + * ```` + * + * @param {Shape} shape - The scale to be measured + * @returns {(String, String) => number} A lambda function providing the + * shape's bounding box coordinates + */ + +export function bounding_box( + shape: Shape, +): (axis: String, min: String) => number { + let bounds: BoundingBox = measureBoundingBox(shape.solid); + return (axis: String, min: String): number => { + let i: number = axis === 'x' ? 0 : axis === 'y' ? 1 : axis === 'z' ? 2 : -1; + let j: number = min === 'min' ? 0 : min === 'max' ? 1 : -1; + if (i === -1 || j === -1) { + throw Error( + 'bounding_box returned function expects a proper axis and min String.', + ); + } else { + return bounds[j][i]; + } + }; +} + +/** + * Rotate the shape by the provided angles in the x, y and z direction. + * Angles provided are in the form of radians (i.e. 2π represent 360 + * degrees) + * + * @param {Shape} shape - The shape to be rotated + * @param {number} x - Angle of rotation in the x direction + * @param {number} y - Angle of rotation in the y direction + * @param {number} z - Angle of rotation in the z direction + * @returns {Shape} The rotated shape + */ +export function rotate(shape: Shape, x: number, y: number, z: number): Shape { + let newSolid: Solid = _rotate([x, y, z], shape.solid); + return new Shape(newSolid); +} + +/** + * Rotate the shape by the provided angles in the x direction. Angles + * provided are in the form of radians (i.e. 2π represent 360 degrees) + * + * @param {Shape} shape - The shape to be rotated + * @param {number} x - Angle of rotation in the x direction + * @returns {Shape} The rotated shape + */ +export function rotate_x(shape: Shape, x: number): Shape { + return rotate(shape, x, 0, 0); +} + +/** + * Rotate the shape by the provided angles in the y direction. Angles + * provided are in the form of radians (i.e. 2π represent 360 degrees) + * + * @param {Shape} shape - The shape to be rotated + * @param {number} y - Angle of rotation in the y direction + * @returns {Shape} The rotated shape + */ +export function rotate_y(shape: Shape, y: number): Shape { + return rotate(shape, 0, y, 0); +} + +/** + * Rotate the shape by the provided angles in the z direction. Angles + * provided are in the form of radians (i.e. 2π represent 360 degrees) + * + * @param {Shape} shape - The shape to be rotated + * @param {number} z - Angle of rotation in the z direction + * @returns {Shape} The rotated shape + */ +export function rotate_z(shape: Shape, z: number): Shape { + return rotate(shape, 0, 0, z); +} + +//TODO +/** + * Center the provided shape with the middle base of the shape at (0, 0, 0). + * + * @param {Shape} shape - The shape to be centered + * @returns {Shape} The shape that is centered + */ +function shapeSetOrigin(shape: Shape) { + let newSolid: Solid = align({ modes: ['min', 'min', 'min'] }, shape.solid); + return new Shape(newSolid); +} + +/** + * Checks if the specified argument is a Shape. + * + * @param {unknown} argument - The value to check. + * @returns {boolean} Whether the argument is a Shape. + */ +export function is_shape(argument: unknown): boolean { + return argument instanceof Shape; +} + +/** + * Creates a clone of the specified Shape. + * + * @param {Shape} shape - The Shape to be cloned. + * @returns {Shape} The cloned Shape. + */ +export function clone(shape: Shape): Shape { + return shape.clone(); +} + +/** + * Stores a clone of the specified Shape for later rendering. Its colour + * defaults to the module's provided silver colour variable. + * + * @param {Shape} shape - The Shape to be stored. + */ +export function store(shape: Shape): void { + store_as_color(shape, DEFAULT_COLOR); +} + +/** + * Colours a clone of the specified Shape using the specified hex colour code, + * then stores it for later rendering. You may use one of the colour variables + * provided by the module, or you may specify your own custom colour code. + * + * Colour codes must be of the form "#XXXXXX" or "XXXXXX", where each X + * represents a non-case sensitive hexadecimal number. Invalid colour codes + * default to black. + * + * @param {Shape} shape - The Shape to be coloured and stored. + * @param {string} hex - The colour code to use. + */ +export function store_as_color(shape: Shape, hex: string): void { + let color: Color = hexToColor(hex); + let coloredSolid: Solid = colorize(color, shape.solid); + Core.getRenderGroupManager() + .storeShape(new Shape(coloredSolid)); +} + +/** + * Colours a clone of the specified Shape using the specified RGB values, then + * stores it for later rendering. + * + * RGB values are clamped between 0 and 1. + * + * @param {Shape} shape - The Shape to be coloured and stored. + * @param {number} redComponent - The colour's red component. + * @param {number} greenComponent - The colour's green component. + * @param {number} blueComponent - The colour's blue component. + */ +export function store_as_rgb( + shape: Shape, + redComponent: number, + greenComponent: number, + blueComponent: number, +): void { + redComponent = clamp(redComponent, 0, 1); + greenComponent = clamp(greenComponent, 0, 1); + blueComponent = clamp(blueComponent, 0, 1); + + let coloredSolid: Solid = colorize( + [redComponent, greenComponent, blueComponent], + shape.solid, + ); + Core.getRenderGroupManager() + .storeShape(new Shape(coloredSolid)); +} + +/** + * Renders using any Shapes stored thus far, along with a grid and axis. The + * Shapes will then not be included in any subsequent renders. + */ +export function render_grid_axis(): RenderGroup { + // Render group is returned for REPL text only; do not document + return Core.getRenderGroupManager() + .nextRenderGroup(true, true); +} + +/** + * Renders using any Shapes stored thus far, along with a grid. The Shapes will + * then not be included in any subsequent renders. + */ +export function render_grid(): RenderGroup { + return Core.getRenderGroupManager() + .nextRenderGroup(true); +} + +/** + * Renders using any Shapes stored thus far, along with an axis. The Shapes will + * then not be included in any subsequent renders. + */ +export function render_axis(): RenderGroup { + return Core.getRenderGroupManager() + .nextRenderGroup(undefined, true); +} + +/** + * Renders using any Shapes stored thus far. The Shapes will then not be + * included in any subsequent renders. + */ +export function render(): RenderGroup { + return Core.getRenderGroupManager() + .nextRenderGroup(); +} diff --git a/src/bundles/csg/index.ts b/src/bundles/csg/index.ts index e6baa0ca0..ade885340 100644 --- a/src/bundles/csg/index.ts +++ b/src/bundles/csg/index.ts @@ -1,153 +1,153 @@ -/* [Imports] */ -import { ModuleContext } from 'js-slang'; -import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; -import { Core } from './core.js'; -import { - area, - beside_x, - beside_y, - beside_z, - black, - blue, - bounding_box, - clone, - cone, - crimson, - cube, - cyan, - cylinder, - flip_x, - flip_y, - flip_z, - geodesic_sphere, - gray, - green, - intersect, - is_shape, - lime, - navy, - orange, - pink, - prism, - purple, - pyramid, - render, - render_axis, - render_grid, - render_grid_axis, - rose, - rotate, - rotate_x, - rotate_y, - rotate_z, - rounded_cube, - rounded_cylinder, - scale, - scale_x, - scale_y, - scale_z, - shape_center, - shape_set_center, - silver, - sphere, - star, - store, - store_as_color, - store_as_rgb, - subtract, - teal, - torus, - translate, - translate_x, - translate_y, - translate_z, - union, - volume, - white, - yellow, -} from './functions'; -import { CsgModuleState, getModuleContext } from './utilities.js'; - -/* [Exports] */ -export default (moduleParams: ModuleParams, moduleContexts: ModuleContexts) => { - let potentialModuleContext: ModuleContext | null = getModuleContext( - moduleContexts - ); - if (potentialModuleContext !== null) { - let moduleContext: ModuleContext = potentialModuleContext; - - let moduleState: CsgModuleState = new CsgModuleState(); - moduleContext.state = moduleState; - Core.initialize(moduleState); - } - - return { - // [Variables - Primitive shapes] - cube, - sphere, - cylinder, - prism, - star, - pyramid, - cone, - torus, - rounded_cube, - rounded_cylinder, - geodesic_sphere, - - // [Variables - Colours] - black, - navy, - green, - teal, - crimson, - purple, - orange, - silver, - gray, - blue, - lime, - cyan, - rose, - pink, - yellow, - white, - - // [Functions] - union, - subtract, - intersect, - scale, - scale_x, - scale_y, - scale_z, - shape_center, - shape_set_center, - area, - volume, - flip_x, - flip_y, - flip_z, - translate, - translate_x, - translate_y, - translate_z, - beside_x, - beside_y, - beside_z, - bounding_box, - rotate, - rotate_x, - rotate_y, - rotate_z, - is_shape, - clone, - store, - store_as_color, - store_as_rgb, - render_grid_axis, - render_grid, - render_axis, - render, - }; -}; +/* [Imports] */ +import { ModuleContext } from 'js-slang'; +import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; +import { Core } from './core.js'; +import { + area, + beside_x, + beside_y, + beside_z, + black, + blue, + bounding_box, + clone, + cone, + crimson, + cube, + cyan, + cylinder, + flip_x, + flip_y, + flip_z, + geodesic_sphere, + gray, + green, + intersect, + is_shape, + lime, + navy, + orange, + pink, + prism, + purple, + pyramid, + render, + render_axis, + render_grid, + render_grid_axis, + rose, + rotate, + rotate_x, + rotate_y, + rotate_z, + rounded_cube, + rounded_cylinder, + scale, + scale_x, + scale_y, + scale_z, + shape_center, + shape_set_center, + silver, + sphere, + star, + store, + store_as_color, + store_as_rgb, + subtract, + teal, + torus, + translate, + translate_x, + translate_y, + translate_z, + union, + volume, + white, + yellow, +} from './functions'; +import { CsgModuleState, getModuleContext } from './utilities.js'; + +/* [Exports] */ +export default (moduleParams: ModuleParams, moduleContexts: ModuleContexts) => { + let potentialModuleContext: ModuleContext | null = getModuleContext( + moduleContexts, + ); + if (potentialModuleContext !== null) { + let moduleContext: ModuleContext = potentialModuleContext; + + let moduleState: CsgModuleState = new CsgModuleState(); + moduleContext.state = moduleState; + Core.initialize(moduleState); + } + + return { + // [Variables - Primitive shapes] + cube, + sphere, + cylinder, + prism, + star, + pyramid, + cone, + torus, + rounded_cube, + rounded_cylinder, + geodesic_sphere, + + // [Variables - Colours] + black, + navy, + green, + teal, + crimson, + purple, + orange, + silver, + gray, + blue, + lime, + cyan, + rose, + pink, + yellow, + white, + + // [Functions] + union, + subtract, + intersect, + scale, + scale_x, + scale_y, + scale_z, + shape_center, + shape_set_center, + area, + volume, + flip_x, + flip_y, + flip_z, + translate, + translate_x, + translate_y, + translate_z, + beside_x, + beside_y, + beside_z, + bounding_box, + rotate, + rotate_x, + rotate_y, + rotate_z, + is_shape, + clone, + store, + store_as_color, + store_as_rgb, + render_grid_axis, + render_grid, + render_axis, + render, + }; +}; diff --git a/src/bundles/csg/renderer.ts b/src/bundles/csg/renderer.ts index 6bc890e11..b5cae2c26 100644 --- a/src/bundles/csg/renderer.ts +++ b/src/bundles/csg/renderer.ts @@ -1,373 +1,371 @@ -/* [Imports] */ -import vec3 from '@jscad/modeling/src/maths/vec3'; -import { - BoundingBox, - measureBoundingBox, -} from '@jscad/modeling/src/measurements'; -import { ACE_GUTTER_BACKGROUND_COLOR } from './constants.js'; -import { - ControlsState, - ControlsUpdate, - ControlsZoomToFit, - Entity, - GeometryEntity, - PerspectiveCameraState, - PrepareRender, - Solid, - WrappedRenderer, -} from './types'; -import { - AxisEntity, - CameraViewportDimensions, - controls, - controlsStateDefaults, - CsgModuleState, - entitiesFromSolids, - FrameTracker, - hexToRgba, - MultiGridEntity, - neatGridDistance, - perspectiveCamera, - perspectiveCameraStateDefaults, - prepareDrawCommands, - prepareRender, - RenderGroup, - Shape, -} from './utilities'; - -/* [Main] */ -function makeWrappedRenderer( - canvas: HTMLCanvasElement -): WrappedRenderer.Function { - let prepareRenderOptions: PrepareRender.AllOptions = { - glOptions: { canvas }, - }; - return prepareRender(prepareRenderOptions); -} - -function addEntities( - renderGroup: RenderGroup, - solids: Solid[], - geometryEntities: GeometryEntity[] -): Entity[] { - let { hasGrid, hasAxis } = renderGroup; - let allEntities: Entity[] = [...geometryEntities]; - - // Run calculations for grid and/or axis only if needed - if (!(hasAxis || hasGrid)) return allEntities; - - let boundingBoxes: BoundingBox[] = solids.map((solid: Solid) => - measureBoundingBox(solid) - ); - let minMaxXys: number[][] = boundingBoxes.map((boundingBox: BoundingBox) => { - let minX = boundingBox[0][0]; - let minY = boundingBox[0][1]; - let maxX = boundingBox[1][0]; - let maxY = boundingBox[1][1]; - return [minX, minY, maxX, maxY]; - }); - let xys: number[] = minMaxXys.flat(1); - let distancesFromOrigin: number[] = xys.map(Math.abs); - let furthestDistance: number = Math.max(...distancesFromOrigin); - let neatDistance: number = neatGridDistance(furthestDistance); - - if (hasGrid) allEntities.push(new MultiGridEntity(neatDistance * 2)); - if (hasAxis) allEntities.push(new AxisEntity(neatDistance)); - return allEntities; -} - -function adjustCameraAngle( - perspectiveCameraState: PerspectiveCameraState, - controlsState: ControlsState | null = null -): void { - if (controlsState === null) { - // Modify the position & view of the passed camera state, - // based on its existing position of the viewer (eye), - // target point the viewer is looking at (centre) & up axis - perspectiveCamera.update(perspectiveCameraState); - return; - } - - let output: ControlsUpdate.Output = controls.update({ - controls: controlsState, - camera: perspectiveCameraState, - }); - - // Manually apply unlike perspectiveCamera.update() - controlsState.thetaDelta = output.controls.thetaDelta; - controlsState.phiDelta = output.controls.phiDelta; - controlsState.scale = output.controls.scale; - - perspectiveCameraState.position = output.camera.position; - perspectiveCameraState.view = output.camera.view; -} - -function doDynamicResize( - canvas: HTMLCanvasElement, - perspectiveCameraState: PerspectiveCameraState -): void { - let canvasBounds: DOMRect = canvas.getBoundingClientRect(); - let { devicePixelRatio } = window; - - // Account for display scaling - let width: number = canvasBounds.width * devicePixelRatio; - let height: number = canvasBounds.height * devicePixelRatio; - - canvas.width = width; - canvas.height = height; - - // Modify the projection, aspect ratio & viewport - perspectiveCamera.setProjection( - perspectiveCameraState, - perspectiveCameraState, - new CameraViewportDimensions(width, height) - ); -} - -function doZoom( - zoomTicks: number, - perspectiveCameraState: PerspectiveCameraState, - controlsState: ControlsState -): void { - while (zoomTicks !== 0) { - let currentTick: number = Math.sign(zoomTicks); - zoomTicks -= currentTick; - - let scaleChange: number = currentTick * 0.1; - let potentialNewScale: number = controlsState.scale + scaleChange; - let potentialNewDistance: number = - vec3.distance( - perspectiveCameraState.position, - perspectiveCameraState.target - ) * potentialNewScale; - - if ( - potentialNewDistance > controlsState.limits.minDistance && - potentialNewDistance < controlsState.limits.maxDistance - ) { - controlsState.scale = potentialNewScale; - } else break; - } - - adjustCameraAngle(perspectiveCameraState, controlsState); -} - -function doZoomToFit( - geometryEntities: GeometryEntity[], - perspectiveCameraState: PerspectiveCameraState, - controlsState: ControlsState -): void { - let options: ControlsZoomToFit.Options = { - controls: controlsState, - camera: perspectiveCameraState, - entities: geometryEntities, - }; - let output: ControlsZoomToFit.Output = controls.zoomToFit(options); - - perspectiveCameraState.target = output.camera.target; - controlsState.scale = output.controls.scale; - - adjustCameraAngle(perspectiveCameraState, controlsState); -} - -function doRotate( - rotateX: number, - rotateY: number, - perspectiveCameraState: PerspectiveCameraState, - controlsState: ControlsState -): void { - let output = controls.rotate( - { - controls: controlsState, - camera: perspectiveCameraState, - speed: 0.0015, - }, - [rotateX, rotateY] - ); - - let newControlsState = output.controls; - controlsState.thetaDelta = newControlsState.thetaDelta; - controlsState.phiDelta = newControlsState.phiDelta; - - adjustCameraAngle(perspectiveCameraState, controlsState); -} - -function doPan( - panX: number, - panY: number, - perspectiveCameraState: PerspectiveCameraState, - controlsState: ControlsState -): void { - let output = controls.pan( - { - controls: controlsState, - camera: perspectiveCameraState, - }, - [panX, panY * 0.75] - ); - - let newCameraState = output.camera; - perspectiveCameraState.position = newCameraState.position; - perspectiveCameraState.target = newCameraState.target; - - adjustCameraAngle(perspectiveCameraState, controlsState); -} - -function registerEvents( - canvas: HTMLCanvasElement, - frameTracker: FrameTracker -): void { - canvas.addEventListener( - 'wheel', - (wheelEvent: WheelEvent) => { - frameTracker.changeZoomTicks(wheelEvent.deltaY); - - wheelEvent.preventDefault(); - }, - { passive: false } - ); - - canvas.addEventListener('dblclick', (_mouseEvent: MouseEvent) => { - frameTracker.setZoomToFit(); - }); - - canvas.addEventListener( - 'pointerdown', - (pointerEvent: PointerEvent) => { - frameTracker.setHeldPointer(pointerEvent.button); - frameTracker.lastX = pointerEvent.pageX; - frameTracker.lastY = pointerEvent.pageY; - - // Detect drags even outside the canvas element's borders - canvas.setPointerCapture(pointerEvent.pointerId); - - pointerEvent.preventDefault(); - }, - { passive: false } - ); - canvas.addEventListener('pointerup', (pointerEvent: PointerEvent) => { - frameTracker.unsetHeldPointer(); - frameTracker.unsetLastCoordinates(); - - canvas.releasePointerCapture(pointerEvent.pointerId); - }); - - canvas.addEventListener('pointermove', (pointerEvent: PointerEvent) => { - let currentX = pointerEvent.pageX; - let currentY = pointerEvent.pageY; - if (frameTracker.lastX < 0 || frameTracker.lastY < 0) { - // If never tracked before, let differences result in 0 - frameTracker.lastX = currentX; - frameTracker.lastY = currentY; - } - - if (!frameTracker.shouldIgnorePointerMove()) { - let differenceX = frameTracker.lastX - currentX; - let differenceY = frameTracker.lastY - currentY; - - if (frameTracker.isPointerPan(pointerEvent.shiftKey)) { - frameTracker.panX += differenceX; - frameTracker.panY -= differenceY; - } else { - // Else default to rotate - frameTracker.rotateX -= differenceX; - frameTracker.rotateY += differenceY; - } - } - - frameTracker.lastX = currentX; - frameTracker.lastY = currentY; - }); -} - -/* [Exports] */ -export default function render( - canvas: HTMLCanvasElement, - moduleState: CsgModuleState -): () => number { - let wrappedRenderer: WrappedRenderer.Function = makeWrappedRenderer(canvas); - - // Create our own state to modify based on the defaults - let perspectiveCameraState: PerspectiveCameraState = { - ...perspectiveCameraStateDefaults, - position: [1000, 1000, 1500], - }; - let controlsState: ControlsState = { - ...controlsStateDefaults, - }; - - let renderGroups: RenderGroup[] = moduleState.renderGroupManager.getGroupsToRender(); - let lastRenderGroup: RenderGroup = renderGroups.at(-1) as RenderGroup; - let solids: Solid[] = lastRenderGroup.shapes.map( - (shape: Shape) => shape.solid - ); - let geometryEntities: GeometryEntity[] = entitiesFromSolids( - undefined, - ...solids - ); - - // Data to pass to the wrapped renderer we made, below - let wrappedRendererData: WrappedRenderer.AllData = { - rendering: { - background: hexToRgba(ACE_GUTTER_BACKGROUND_COLOR), - }, - - entities: addEntities(lastRenderGroup, solids, geometryEntities), - drawCommands: prepareDrawCommands, - camera: perspectiveCameraState, - }; - - // Custom object to track processing - let frameTracker: FrameTracker = new FrameTracker(); - - let requestId: number = 0; - - // Create a callback function. - // Request animation frame with it once; it will loop itself from there - function animationCallback(_timestamp: DOMHighResTimeStamp) { - doDynamicResize(canvas, perspectiveCameraState); - - if (frameTracker.shouldZoom()) { - doZoom( - frameTracker.getZoomTicks(), - perspectiveCameraState, - controlsState - ); - frameTracker.didZoom(); - } - - if (frameTracker.shouldZoomToFit()) { - doZoomToFit(geometryEntities, perspectiveCameraState, controlsState); - frameTracker.didZoomToFit(); - } - - if (frameTracker.shouldRotate()) { - doRotate( - frameTracker.rotateX, - frameTracker.rotateY, - perspectiveCameraState, - controlsState - ); - frameTracker.didRotate(); - } - - if (frameTracker.shouldPan()) { - doPan( - frameTracker.panX, - frameTracker.panY, - perspectiveCameraState, - controlsState - ); - frameTracker.didPan(); - } - - wrappedRenderer(wrappedRendererData); - - requestId = window.requestAnimationFrame(animationCallback); - } - requestId = window.requestAnimationFrame(animationCallback); - - registerEvents(canvas, frameTracker); - - return () => requestId; -} +/* [Imports] */ +import vec3 from '@jscad/modeling/src/maths/vec3'; +import { + BoundingBox, + measureBoundingBox, +} from '@jscad/modeling/src/measurements'; +import { ACE_GUTTER_BACKGROUND_COLOR } from './constants.js'; +import { + ControlsState, + ControlsUpdate, + ControlsZoomToFit, + Entity, + GeometryEntity, + PerspectiveCameraState, + PrepareRender, + Solid, + WrappedRenderer, +} from './types'; +import { + AxisEntity, + CameraViewportDimensions, + controls, + controlsStateDefaults, + CsgModuleState, + entitiesFromSolids, + FrameTracker, + hexToRgba, + MultiGridEntity, + neatGridDistance, + perspectiveCamera, + perspectiveCameraStateDefaults, + prepareDrawCommands, + prepareRender, + RenderGroup, + Shape, +} from './utilities'; + +/* [Main] */ +function makeWrappedRenderer( + canvas: HTMLCanvasElement, +): WrappedRenderer.Function { + let prepareRenderOptions: PrepareRender.AllOptions = { + glOptions: { canvas }, + }; + return prepareRender(prepareRenderOptions); +} + +function addEntities( + renderGroup: RenderGroup, + solids: Solid[], + geometryEntities: GeometryEntity[], +): Entity[] { + let { hasGrid, hasAxis } = renderGroup; + let allEntities: Entity[] = [...geometryEntities]; + + // Run calculations for grid and/or axis only if needed + if (!(hasAxis || hasGrid)) return allEntities; + + let boundingBoxes: BoundingBox[] = solids.map((solid: Solid) => measureBoundingBox(solid)); + let minMaxXys: number[][] = boundingBoxes.map((boundingBox: BoundingBox) => { + let minX = boundingBox[0][0]; + let minY = boundingBox[0][1]; + let maxX = boundingBox[1][0]; + let maxY = boundingBox[1][1]; + return [minX, minY, maxX, maxY]; + }); + let xys: number[] = minMaxXys.flat(1); + let distancesFromOrigin: number[] = xys.map(Math.abs); + let furthestDistance: number = Math.max(...distancesFromOrigin); + let neatDistance: number = neatGridDistance(furthestDistance); + + if (hasGrid) allEntities.push(new MultiGridEntity(neatDistance * 2)); + if (hasAxis) allEntities.push(new AxisEntity(neatDistance)); + return allEntities; +} + +function adjustCameraAngle( + perspectiveCameraState: PerspectiveCameraState, + controlsState: ControlsState | null = null, +): void { + if (controlsState === null) { + // Modify the position & view of the passed camera state, + // based on its existing position of the viewer (eye), + // target point the viewer is looking at (centre) & up axis + perspectiveCamera.update(perspectiveCameraState); + return; + } + + let output: ControlsUpdate.Output = controls.update({ + controls: controlsState, + camera: perspectiveCameraState, + }); + + // Manually apply unlike perspectiveCamera.update() + controlsState.thetaDelta = output.controls.thetaDelta; + controlsState.phiDelta = output.controls.phiDelta; + controlsState.scale = output.controls.scale; + + perspectiveCameraState.position = output.camera.position; + perspectiveCameraState.view = output.camera.view; +} + +function doDynamicResize( + canvas: HTMLCanvasElement, + perspectiveCameraState: PerspectiveCameraState, +): void { + let canvasBounds: DOMRect = canvas.getBoundingClientRect(); + let { devicePixelRatio } = window; + + // Account for display scaling + let width: number = canvasBounds.width * devicePixelRatio; + let height: number = canvasBounds.height * devicePixelRatio; + + canvas.width = width; + canvas.height = height; + + // Modify the projection, aspect ratio & viewport + perspectiveCamera.setProjection( + perspectiveCameraState, + perspectiveCameraState, + new CameraViewportDimensions(width, height), + ); +} + +function doZoom( + zoomTicks: number, + perspectiveCameraState: PerspectiveCameraState, + controlsState: ControlsState, +): void { + while (zoomTicks !== 0) { + let currentTick: number = Math.sign(zoomTicks); + zoomTicks -= currentTick; + + let scaleChange: number = currentTick * 0.1; + let potentialNewScale: number = controlsState.scale + scaleChange; + let potentialNewDistance: number + = vec3.distance( + perspectiveCameraState.position, + perspectiveCameraState.target, + ) * potentialNewScale; + + if ( + potentialNewDistance > controlsState.limits.minDistance + && potentialNewDistance < controlsState.limits.maxDistance + ) { + controlsState.scale = potentialNewScale; + } else break; + } + + adjustCameraAngle(perspectiveCameraState, controlsState); +} + +function doZoomToFit( + geometryEntities: GeometryEntity[], + perspectiveCameraState: PerspectiveCameraState, + controlsState: ControlsState, +): void { + let options: ControlsZoomToFit.Options = { + controls: controlsState, + camera: perspectiveCameraState, + entities: geometryEntities, + }; + let output: ControlsZoomToFit.Output = controls.zoomToFit(options); + + perspectiveCameraState.target = output.camera.target; + controlsState.scale = output.controls.scale; + + adjustCameraAngle(perspectiveCameraState, controlsState); +} + +function doRotate( + rotateX: number, + rotateY: number, + perspectiveCameraState: PerspectiveCameraState, + controlsState: ControlsState, +): void { + let output = controls.rotate( + { + controls: controlsState, + camera: perspectiveCameraState, + speed: 0.0015, + }, + [rotateX, rotateY], + ); + + let newControlsState = output.controls; + controlsState.thetaDelta = newControlsState.thetaDelta; + controlsState.phiDelta = newControlsState.phiDelta; + + adjustCameraAngle(perspectiveCameraState, controlsState); +} + +function doPan( + panX: number, + panY: number, + perspectiveCameraState: PerspectiveCameraState, + controlsState: ControlsState, +): void { + let output = controls.pan( + { + controls: controlsState, + camera: perspectiveCameraState, + }, + [panX, panY * 0.75], + ); + + let newCameraState = output.camera; + perspectiveCameraState.position = newCameraState.position; + perspectiveCameraState.target = newCameraState.target; + + adjustCameraAngle(perspectiveCameraState, controlsState); +} + +function registerEvents( + canvas: HTMLCanvasElement, + frameTracker: FrameTracker, +): void { + canvas.addEventListener( + 'wheel', + (wheelEvent: WheelEvent) => { + frameTracker.changeZoomTicks(wheelEvent.deltaY); + + wheelEvent.preventDefault(); + }, + { passive: false }, + ); + + canvas.addEventListener('dblclick', (_mouseEvent: MouseEvent) => { + frameTracker.setZoomToFit(); + }); + + canvas.addEventListener( + 'pointerdown', + (pointerEvent: PointerEvent) => { + frameTracker.setHeldPointer(pointerEvent.button); + frameTracker.lastX = pointerEvent.pageX; + frameTracker.lastY = pointerEvent.pageY; + + // Detect drags even outside the canvas element's borders + canvas.setPointerCapture(pointerEvent.pointerId); + + pointerEvent.preventDefault(); + }, + { passive: false }, + ); + canvas.addEventListener('pointerup', (pointerEvent: PointerEvent) => { + frameTracker.unsetHeldPointer(); + frameTracker.unsetLastCoordinates(); + + canvas.releasePointerCapture(pointerEvent.pointerId); + }); + + canvas.addEventListener('pointermove', (pointerEvent: PointerEvent) => { + let currentX = pointerEvent.pageX; + let currentY = pointerEvent.pageY; + if (frameTracker.lastX < 0 || frameTracker.lastY < 0) { + // If never tracked before, let differences result in 0 + frameTracker.lastX = currentX; + frameTracker.lastY = currentY; + } + + if (!frameTracker.shouldIgnorePointerMove()) { + let differenceX = frameTracker.lastX - currentX; + let differenceY = frameTracker.lastY - currentY; + + if (frameTracker.isPointerPan(pointerEvent.shiftKey)) { + frameTracker.panX += differenceX; + frameTracker.panY -= differenceY; + } else { + // Else default to rotate + frameTracker.rotateX -= differenceX; + frameTracker.rotateY += differenceY; + } + } + + frameTracker.lastX = currentX; + frameTracker.lastY = currentY; + }); +} + +/* [Exports] */ +export default function render( + canvas: HTMLCanvasElement, + moduleState: CsgModuleState, +): () => number { + let wrappedRenderer: WrappedRenderer.Function = makeWrappedRenderer(canvas); + + // Create our own state to modify based on the defaults + let perspectiveCameraState: PerspectiveCameraState = { + ...perspectiveCameraStateDefaults, + position: [1000, 1000, 1500], + }; + let controlsState: ControlsState = { + ...controlsStateDefaults, + }; + + let renderGroups: RenderGroup[] = moduleState.renderGroupManager.getGroupsToRender(); + let lastRenderGroup: RenderGroup = renderGroups.at(-1) as RenderGroup; + let solids: Solid[] = lastRenderGroup.shapes.map( + (shape: Shape) => shape.solid, + ); + let geometryEntities: GeometryEntity[] = entitiesFromSolids( + undefined, + ...solids, + ); + + // Data to pass to the wrapped renderer we made, below + let wrappedRendererData: WrappedRenderer.AllData = { + rendering: { + background: hexToRgba(ACE_GUTTER_BACKGROUND_COLOR), + }, + + entities: addEntities(lastRenderGroup, solids, geometryEntities), + drawCommands: prepareDrawCommands, + camera: perspectiveCameraState, + }; + + // Custom object to track processing + let frameTracker: FrameTracker = new FrameTracker(); + + let requestId: number = 0; + + // Create a callback function. + // Request animation frame with it once; it will loop itself from there + function animationCallback(_timestamp: DOMHighResTimeStamp) { + doDynamicResize(canvas, perspectiveCameraState); + + if (frameTracker.shouldZoom()) { + doZoom( + frameTracker.getZoomTicks(), + perspectiveCameraState, + controlsState, + ); + frameTracker.didZoom(); + } + + if (frameTracker.shouldZoomToFit()) { + doZoomToFit(geometryEntities, perspectiveCameraState, controlsState); + frameTracker.didZoomToFit(); + } + + if (frameTracker.shouldRotate()) { + doRotate( + frameTracker.rotateX, + frameTracker.rotateY, + perspectiveCameraState, + controlsState, + ); + frameTracker.didRotate(); + } + + if (frameTracker.shouldPan()) { + doPan( + frameTracker.panX, + frameTracker.panY, + perspectiveCameraState, + controlsState, + ); + frameTracker.didPan(); + } + + wrappedRenderer(wrappedRendererData); + + requestId = window.requestAnimationFrame(animationCallback); + } + requestId = window.requestAnimationFrame(animationCallback); + + registerEvents(canvas, frameTracker); + + return () => requestId; +} diff --git a/src/bundles/csg/types.ts b/src/bundles/csg/types.ts index d4ee43d38..09a812d8d 100644 --- a/src/bundles/csg/types.ts +++ b/src/bundles/csg/types.ts @@ -1,349 +1,349 @@ -/* [Imports] */ -import { RGB, RGBA } from '@jscad/modeling/src/colors'; -import { Geom3 } from '@jscad/modeling/src/geometries/types'; -import { - cameras, - controls as _controls, - drawCommands, -} from '@jscad/regl-renderer'; -import makeDrawMultiGrid from '@jscad/regl-renderer/types/rendering/commands/drawGrid/multi'; -import { InitializationOptions } from 'regl'; - -/* [Main] */ -let orthographicCamera = cameras.orthographic; -let perspectiveCamera = cameras.perspective; - -let controls = _controls.orbit; - -/* [Exports] */ - -// [Proper typing for JS in regl-renderer] -type Numbers2 = [number, number]; - -type Numbers3 = [number, number, number]; -export type VectorXYZ = Numbers3; -export type CoordinatesXYZ = Numbers3; -export type Color = RGB; - -export type Mat4 = Float32Array; - -// @jscad\regl-renderer\src\cameras\perspectiveCamera.js -// @jscad\regl-renderer\src\cameras\orthographicCamera.js -export type PerspectiveCamera = typeof perspectiveCamera; -export type OrthographicCamera = typeof orthographicCamera; - -export type PerspectiveCameraState = Omit< - typeof perspectiveCamera.cameraState, - 'target' | 'position' | 'view' -> & { - target: CoordinatesXYZ; - - position: CoordinatesXYZ; - view: Mat4; -}; -export type OrthographicCameraState = typeof orthographicCamera.cameraState; -export type CameraState = PerspectiveCameraState | OrthographicCameraState; - -// @jscad\regl-renderer\src\controls\orbitControls.js -export type Controls = Omit< - typeof controls, - 'update' | 'zoomToFit' | 'rotate' | 'pan' -> & { - update: ControlsUpdate.Function; - zoomToFit: ControlsZoomToFit.Function; - rotate: ControlsRotate; - pan: ControlsPan; -}; -export namespace ControlsUpdate { - export type Function = (options: Options) => Output; - - export type Options = { - controls: ControlsState; - camera: CameraState; - }; - - export type Output = { - controls: { - thetaDelta: number; - phiDelta: number; - scale: number; - changed: boolean; - }; - camera: { - position: CoordinatesXYZ; - view: Mat4; - }; - }; -} -export namespace ControlsZoomToFit { - export type Function = (options: Options) => Output; - - export type Options = { - controls: ControlsState; - camera: CameraState; - entities: GeometryEntity[]; - }; - - export type Output = { - camera: { - target: VectorXYZ; - }; - controls: { - scale: number; - }; - }; -} -export type ControlsRotate = ( - options: { - controls: ControlsState; - camera: CameraState; - speed?: number; - }, - rotateAngles: Numbers2 -) => { - controls: { - thetaDelta: number; - phiDelta: number; - }; - camera: CameraState; -}; -export type ControlsPan = ( - options: { - controls: ControlsState; - camera: CameraState; - speed?: number; - }, - rotateAngles: Numbers2 -) => { - controls: ControlsState; - camera: { - position: CoordinatesXYZ; - target: VectorXYZ; - }; -}; - -export type ControlsState = Omit< - typeof controls.controlsState, - 'scale' | 'thetaDelta' | 'phiDelta' -> & - typeof controls.controlsProps & { - scale: number; - - thetaDelta: number; - phiDelta: number; - }; - -export type Solid = Geom3; - -// @jscad\regl-renderer\src\geometry-utils-V2\geom3ToGeometries.js -// @jscad\regl-renderer\src\geometry-utils-V2\geom3ToGeometries.test.js -export type Geometry = { - type: '2d' | '3d'; - positions: CoordinatesXYZ[]; - normals: CoordinatesXYZ[]; - indices: CoordinatesXYZ[]; - colors: RGBA[]; - transforms: Mat4; - isTransparent: boolean; -}; - -// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js -// @jscad\regl-renderer\demo-web.js -// There are still other Props used for uniforms in the various rendering -// commands, eg model, color, angle -export type Entity = { - visuals: { - // Key for the draw command that should be used on this Entity. - // Key is used on WrappedRenderer.AllData#drawCommands. - // Property must exist & match a drawCommand, - // or behaviour is like show: false - drawCmd: 'drawAxis' | 'drawGrid' | 'drawLines' | 'drawMesh'; - - // Whether to actually draw the Entity via nested DrawCommand - show: boolean; - }; -}; - -// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js -export type GeometryEntity = Entity & { - visuals: { - drawCmd: 'drawLines' | 'drawMesh'; - - // Whether the Geometry is transparent. - // Transparents need to be rendered before non-transparents - transparent: boolean; - - // Eventually determines whether to use vColorShaders - // (Geometry must also have colour) or meshShaders - useVertexColors: boolean; - }; - - // The original Geometry used to make the GeometryEntity - geometry: Geometry; -}; - -// @jscad\regl-renderer\src\rendering\commands\drawAxis\index.js -// @jscad\regl-renderer\demo-web.js -export type AxisEntityType = Entity & { - visuals: { - drawCmd: 'drawAxis'; - }; - - xColor?: RGBA; - yColor?: RGBA; - zColor?: RGBA; - size?: number; - alwaysVisible?: boolean; - - // Deprecated - lineWidth?: number; -}; - -// @jscad\regl-renderer\src\rendering\commands\drawGrid\index.js -// @jscad\regl-renderer\demo-web.js -export type GridEntity = Entity & { - visuals: { - drawCmd: 'drawGrid'; - - color?: RGBA; - fadeOut?: boolean; - }; - size?: Numbers2; - ticks?: number; - centered?: boolean; - - // Deprecated - lineWidth?: number; -}; - -// @jscad\regl-renderer\src\rendering\commands\drawGrid\multi.js -// @jscad\regl-renderer\demo-web.js -// @jscad\web\src\ui\views\viewer.js -// @jscad\regl-renderer\src\index.js -export type MultiGridEntityType = Omit & { - // Entity#visuals gets stuffed into the nested DrawCommand as Props. - // The Props get passed on wholesale by makeDrawMultiGrid()'s returned lambda, - // where the following properties then get used - // (rather than while setting up the DrawCommands) - visuals: { - subColor?: RGBA; // As color - }; - - // First number used on the main grid, second number on sub grid - ticks?: [number, number]; -}; - -// @jscad\regl-renderer\src\rendering\commands\drawLines\index.js -export type LinesEntity = Entity & { - visuals: { - drawCmd: 'drawLines'; - }; - - color?: RGBA; -}; - -// @jscad\regl-renderer\src\rendering\commands\drawMesh\index.js -export type MeshEntity = Entity & { - visuals: { - drawCmd: 'drawMesh'; - }; - - dynamicCulling?: boolean; - color?: RGBA; -}; - -export namespace PrepareRender { - // @jscad\regl-renderer\src\rendering\render.js - export type Function = (options: AllOptions) => WrappedRenderer.Function; - - // @jscad\regl-renderer\src\rendering\render.js - export type AllOptions = { - // Used to initialise Regl from the REGL package constructor - glOptions: InitializationOptions; - }; -} - -// When called, the WrappedRenderer creates a main DrawCommand. -// This main DrawCommand then gets called as a scoped command, -// used to create & call more DrawCommands for the #entities. -// Nested DrawCommands get cached -// & may store some Entity properties during setup, -// but properties passed in from Props later may take precedence. -// The main DrawCommand is said to be in charge of injecting most uniforms into -// the Regl context, ie keeping track of all Regl global state -export namespace WrappedRenderer { - // @jscad\regl-renderer\src\rendering\render.js - export type Function = (data: AllData) => void; - - // @jscad\regl-renderer\src\rendering\render.js - // Gets used in the WrappedRenderer. - // Also gets passed as Props into the main DrawCommand, - // where it is used in setup specified by the internal - // renderContext.js/renderWrapper. - // The lambda of the main DrawCommand does not use those Props, rather, - // it references the data in the WrappedRenderer directly. - // Therefore, regl.prop() is not called, - // nor are Props used via the semantic equivalent (context, props) => {}. - // The context passed to that lambda also remains unused - export type AllData = { - rendering?: RenderOptions; - - entities: Entity[]; - - drawCommands: PrepareDrawCommands; - - // Along with all of the relevant Entity's & Entity#visuals's properties, - // this gets stuffed into each nested DrawCommand as Props. - // Messy & needs tidying in regl-renderer - camera: CameraState; - }; - - // @jscad\regl-renderer\src\rendering\renderDefaults.js - export type RenderOptions = { - // Custom value used early on in render.js. - // Clears the canvas to this background colour - background?: RGBA; - - // Default value used directly in V2's entitiesFromSolids.js as the default Geometry colour. - // Default value also used directly in various rendering commands as their shader uniforms' default colour. - // Custom value appears unused - meshColor?: RGBA; - - // Custom value used in various rendering commands as shader uniforms - lightColor?: RGBA; - lightDirection?: VectorXYZ; - ambientLightAmount?: number; - diffuseLightAmount?: number; - specularLightAmount?: number; - materialShininess?: number; // As uMaterialShininess in main DrawCommand - - // Unused - lightPosition?: CoordinatesXYZ; // See also lightDirection - }; - - // There are 4 rendering commands to use in regl-renderer: - // drawAxis, drawGrid, drawLines & drawMesh. - // drawExps appears abandoned. - // Only once passed Regl & an Entity do they return an actual DrawCommand - export type PrepareDrawCommands = Record; - export type PrepareDrawCommandFunction = - | typeof drawCommands - | typeof makeDrawMultiGrid; -} - -// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js -// Converts Solids into Geometries and then into Entities -export namespace EntitiesFromSolids { - export type Function = ( - options?: Options, - ...solids: Solid[] - ) => GeometryEntity[]; - - export type Options = { - // Default colour for entity rendering if the solid does not have one - color?: RGBA; - - // Whether to smooth the normals of 3D solids, rendering a smooth surface - smoothNormals?: boolean; - }; -} +/* [Imports] */ +import { RGB, RGBA } from '@jscad/modeling/src/colors'; +import { Geom3 } from '@jscad/modeling/src/geometries/types'; +import { + cameras, + controls as _controls, + drawCommands, +} from '@jscad/regl-renderer'; +import makeDrawMultiGrid from '@jscad/regl-renderer/types/rendering/commands/drawGrid/multi'; +import { InitializationOptions } from 'regl'; + +/* [Main] */ +let orthographicCamera = cameras.orthographic; +let perspectiveCamera = cameras.perspective; + +let controls = _controls.orbit; + +/* [Exports] */ + +// [Proper typing for JS in regl-renderer] +type Numbers2 = [number, number]; + +type Numbers3 = [number, number, number]; +export type VectorXYZ = Numbers3; +export type CoordinatesXYZ = Numbers3; +export type Color = RGB; + +export type Mat4 = Float32Array; + +// @jscad\regl-renderer\src\cameras\perspectiveCamera.js +// @jscad\regl-renderer\src\cameras\orthographicCamera.js +export type PerspectiveCamera = typeof perspectiveCamera; +export type OrthographicCamera = typeof orthographicCamera; + +export type PerspectiveCameraState = Omit< + typeof perspectiveCamera.cameraState, +'target' | 'position' | 'view' +> & { + target: CoordinatesXYZ; + + position: CoordinatesXYZ; + view: Mat4; +}; +export type OrthographicCameraState = typeof orthographicCamera.cameraState; +export type CameraState = PerspectiveCameraState | OrthographicCameraState; + +// @jscad\regl-renderer\src\controls\orbitControls.js +export type Controls = Omit< + typeof controls, +'update' | 'zoomToFit' | 'rotate' | 'pan' +> & { + update: ControlsUpdate.Function; + zoomToFit: ControlsZoomToFit.Function; + rotate: ControlsRotate; + pan: ControlsPan; +}; +export namespace ControlsUpdate { + export type Function = (options: Options) => Output; + + export type Options = { + controls: ControlsState; + camera: CameraState; + }; + + export type Output = { + controls: { + thetaDelta: number; + phiDelta: number; + scale: number; + changed: boolean; + }; + camera: { + position: CoordinatesXYZ; + view: Mat4; + }; + }; +} +export namespace ControlsZoomToFit { + export type Function = (options: Options) => Output; + + export type Options = { + controls: ControlsState; + camera: CameraState; + entities: GeometryEntity[]; + }; + + export type Output = { + camera: { + target: VectorXYZ; + }; + controls: { + scale: number; + }; + }; +} +export type ControlsRotate = ( + options: { + controls: ControlsState; + camera: CameraState; + speed?: number; + }, + rotateAngles: Numbers2 +) => { + controls: { + thetaDelta: number; + phiDelta: number; + }; + camera: CameraState; +}; +export type ControlsPan = ( + options: { + controls: ControlsState; + camera: CameraState; + speed?: number; + }, + rotateAngles: Numbers2 +) => { + controls: ControlsState; + camera: { + position: CoordinatesXYZ; + target: VectorXYZ; + }; +}; + +export type ControlsState = Omit< + typeof controls.controlsState, +'scale' | 'thetaDelta' | 'phiDelta' +> & + typeof controls.controlsProps & { + scale: number; + + thetaDelta: number; + phiDelta: number; +}; + +export type Solid = Geom3; + +// @jscad\regl-renderer\src\geometry-utils-V2\geom3ToGeometries.js +// @jscad\regl-renderer\src\geometry-utils-V2\geom3ToGeometries.test.js +export type Geometry = { + type: '2d' | '3d'; + positions: CoordinatesXYZ[]; + normals: CoordinatesXYZ[]; + indices: CoordinatesXYZ[]; + colors: RGBA[]; + transforms: Mat4; + isTransparent: boolean; +}; + +// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js +// @jscad\regl-renderer\demo-web.js +// There are still other Props used for uniforms in the various rendering +// commands, eg model, color, angle +export type Entity = { + visuals: { + // Key for the draw command that should be used on this Entity. + // Key is used on WrappedRenderer.AllData#drawCommands. + // Property must exist & match a drawCommand, + // or behaviour is like show: false + drawCmd: 'drawAxis' | 'drawGrid' | 'drawLines' | 'drawMesh'; + + // Whether to actually draw the Entity via nested DrawCommand + show: boolean; + }; +}; + +// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js +export type GeometryEntity = Entity & { + visuals: { + drawCmd: 'drawLines' | 'drawMesh'; + + // Whether the Geometry is transparent. + // Transparents need to be rendered before non-transparents + transparent: boolean; + + // Eventually determines whether to use vColorShaders + // (Geometry must also have colour) or meshShaders + useVertexColors: boolean; + }; + + // The original Geometry used to make the GeometryEntity + geometry: Geometry; +}; + +// @jscad\regl-renderer\src\rendering\commands\drawAxis\index.js +// @jscad\regl-renderer\demo-web.js +export type AxisEntityType = Entity & { + visuals: { + drawCmd: 'drawAxis'; + }; + + xColor?: RGBA; + yColor?: RGBA; + zColor?: RGBA; + size?: number; + alwaysVisible?: boolean; + + // Deprecated + lineWidth?: number; +}; + +// @jscad\regl-renderer\src\rendering\commands\drawGrid\index.js +// @jscad\regl-renderer\demo-web.js +export type GridEntity = Entity & { + visuals: { + drawCmd: 'drawGrid'; + + color?: RGBA; + fadeOut?: boolean; + }; + size?: Numbers2; + ticks?: number; + centered?: boolean; + + // Deprecated + lineWidth?: number; +}; + +// @jscad\regl-renderer\src\rendering\commands\drawGrid\multi.js +// @jscad\regl-renderer\demo-web.js +// @jscad\web\src\ui\views\viewer.js +// @jscad\regl-renderer\src\index.js +export type MultiGridEntityType = Omit & { + // Entity#visuals gets stuffed into the nested DrawCommand as Props. + // The Props get passed on wholesale by makeDrawMultiGrid()'s returned lambda, + // where the following properties then get used + // (rather than while setting up the DrawCommands) + visuals: { + subColor?: RGBA; // As color + }; + + // First number used on the main grid, second number on sub grid + ticks?: [number, number]; +}; + +// @jscad\regl-renderer\src\rendering\commands\drawLines\index.js +export type LinesEntity = Entity & { + visuals: { + drawCmd: 'drawLines'; + }; + + color?: RGBA; +}; + +// @jscad\regl-renderer\src\rendering\commands\drawMesh\index.js +export type MeshEntity = Entity & { + visuals: { + drawCmd: 'drawMesh'; + }; + + dynamicCulling?: boolean; + color?: RGBA; +}; + +export namespace PrepareRender { + // @jscad\regl-renderer\src\rendering\render.js + export type Function = (options: AllOptions) => WrappedRenderer.Function; + + // @jscad\regl-renderer\src\rendering\render.js + export type AllOptions = { + // Used to initialise Regl from the REGL package constructor + glOptions: InitializationOptions; + }; +} + +// When called, the WrappedRenderer creates a main DrawCommand. +// This main DrawCommand then gets called as a scoped command, +// used to create & call more DrawCommands for the #entities. +// Nested DrawCommands get cached +// & may store some Entity properties during setup, +// but properties passed in from Props later may take precedence. +// The main DrawCommand is said to be in charge of injecting most uniforms into +// the Regl context, ie keeping track of all Regl global state +export namespace WrappedRenderer { + // @jscad\regl-renderer\src\rendering\render.js + export type Function = (data: AllData) => void; + + // @jscad\regl-renderer\src\rendering\render.js + // Gets used in the WrappedRenderer. + // Also gets passed as Props into the main DrawCommand, + // where it is used in setup specified by the internal + // renderContext.js/renderWrapper. + // The lambda of the main DrawCommand does not use those Props, rather, + // it references the data in the WrappedRenderer directly. + // Therefore, regl.prop() is not called, + // nor are Props used via the semantic equivalent (context, props) => {}. + // The context passed to that lambda also remains unused + export type AllData = { + rendering?: RenderOptions; + + entities: Entity[]; + + drawCommands: PrepareDrawCommands; + + // Along with all of the relevant Entity's & Entity#visuals's properties, + // this gets stuffed into each nested DrawCommand as Props. + // Messy & needs tidying in regl-renderer + camera: CameraState; + }; + + // @jscad\regl-renderer\src\rendering\renderDefaults.js + export type RenderOptions = { + // Custom value used early on in render.js. + // Clears the canvas to this background colour + background?: RGBA; + + // Default value used directly in V2's entitiesFromSolids.js as the default Geometry colour. + // Default value also used directly in various rendering commands as their shader uniforms' default colour. + // Custom value appears unused + meshColor?: RGBA; + + // Custom value used in various rendering commands as shader uniforms + lightColor?: RGBA; + lightDirection?: VectorXYZ; + ambientLightAmount?: number; + diffuseLightAmount?: number; + specularLightAmount?: number; + materialShininess?: number; // As uMaterialShininess in main DrawCommand + + // Unused + lightPosition?: CoordinatesXYZ; // See also lightDirection + }; + + // There are 4 rendering commands to use in regl-renderer: + // drawAxis, drawGrid, drawLines & drawMesh. + // drawExps appears abandoned. + // Only once passed Regl & an Entity do they return an actual DrawCommand + export type PrepareDrawCommands = Record; + export type PrepareDrawCommandFunction = + | typeof drawCommands + | typeof makeDrawMultiGrid; +} + +// @jscad\regl-renderer\src\geometry-utils-V2\entitiesFromSolids.js +// Converts Solids into Geometries and then into Entities +export namespace EntitiesFromSolids { + export type Function = ( + options?: Options, + ...solids: Solid[] + ) => GeometryEntity[]; + + export type Options = { + // Default colour for entity rendering if the solid does not have one + color?: RGBA; + + // Whether to smooth the normals of 3D solids, rendering a smooth surface + smoothNormals?: boolean; + }; +} diff --git a/src/bundles/csg/utilities.ts b/src/bundles/csg/utilities.ts index f668ddcc7..6fe18c379 100644 --- a/src/bundles/csg/utilities.ts +++ b/src/bundles/csg/utilities.ts @@ -1,339 +1,339 @@ -/* [Imports] */ -import { RGBA } from '@jscad/modeling/src/colors'; -import { clone, Geom3 } from '@jscad/modeling/src/geometries/geom3'; -import { - cameras, - controls as _controls, - drawCommands, - entitiesFromSolids as _entitiesFromSolids, - prepareRender as _prepareRender, -} from '@jscad/regl-renderer'; -import { ModuleContext, ModuleState } from 'js-slang'; -import { ModuleContexts } from '../../typings/type_helpers.js'; -import { - ACE_GUTTER_TEXT_COLOR, - BP_TEXT_COLOR, - GRID_PADDING, - MAIN_TICKS, - ROUND_UP_INTERVAL, - SUB_TICKS, -} from './constants.js'; -import { - AxisEntityType, - Color, - Controls, - ControlsState, - EntitiesFromSolids, - MultiGridEntityType, - PerspectiveCamera, - PerspectiveCameraState, - PrepareRender, - Solid, - WrappedRenderer, -} from './types'; - -/* [Exports] */ - -// [Proper typing for JS in regl-renderer] -export const perspectiveCamera: PerspectiveCamera = cameras.perspective; -export const perspectiveCameraStateDefaults: PerspectiveCameraState = - perspectiveCamera.defaults; - -export const controls: Controls = (_controls.orbit as unknown) as Controls; -export const controlsStateDefaults: ControlsState = controls.defaults; - -export const prepareRender: PrepareRender.Function = _prepareRender; - -export const entitiesFromSolids: EntitiesFromSolids.Function = (_entitiesFromSolids as unknown) as EntitiesFromSolids.Function; -export const prepareDrawCommands: WrappedRenderer.PrepareDrawCommands = drawCommands; - -// [Custom] -export class MultiGridEntity implements MultiGridEntityType { - visuals: { - drawCmd: 'drawGrid'; - show: boolean; - color?: RGBA; - subColor?: RGBA; - } = { - drawCmd: 'drawGrid', - show: true, - - color: hexToRgba(BP_TEXT_COLOR), - subColor: hexToRgba(ACE_GUTTER_TEXT_COLOR), - }; - - ticks: [number, number] = [MAIN_TICKS, SUB_TICKS]; - - size: [number, number]; - - constructor(size: number) { - this.size = [size, size]; - } -} - -export class AxisEntity implements AxisEntityType { - visuals: { - drawCmd: 'drawAxis'; - show: boolean; - } = { - drawCmd: 'drawAxis', - show: true, - }; - - alwaysVisible: boolean = false; - - constructor(public size?: number) {} -} - -export class Shape { - constructor(public solid: Solid) {} - - clone(): Shape { - return new Shape(clone(this.solid as Geom3)); - } - - toReplString(): string { - return ''; - } -} - -export class RenderGroup { - constructor(public canvasNumber: number) {} - - render: boolean = false; - hasGrid: boolean = true; - hasAxis: boolean = true; - - shapes: Shape[] = []; - - toReplString(): string { - return ``; - } -} - -export class RenderGroupManager { - private canvasTracker: number = 1; - private renderGroups: RenderGroup[] = []; - - constructor() { - this.addRenderGroup(); - } - - private addRenderGroup(): void { - // Passes in canvasTracker as is, then increments it - this.renderGroups.push(new RenderGroup(this.canvasTracker++)); - } - - private getCurrentRenderGroup(): RenderGroup { - return this.renderGroups.at(-1) as RenderGroup; - } - - nextRenderGroup( - currentGrid: boolean = false, - currentAxis: boolean = false - ): RenderGroup { - let previousRenderGroup: RenderGroup = this.getCurrentRenderGroup(); - previousRenderGroup.render = true; - previousRenderGroup.hasGrid = currentGrid; - previousRenderGroup.hasAxis = currentAxis; - - this.addRenderGroup(); - - return previousRenderGroup; - } - - storeShape(shape: Shape): void { - this.getCurrentRenderGroup().shapes.push(shape); - } - - shouldRender(): boolean { - return this.getGroupsToRender().length > 0; - } - - getGroupsToRender(): RenderGroup[] { - return this.renderGroups.filter( - (renderGroup: RenderGroup) => renderGroup.render - ); - } -} - -export class CsgModuleState implements ModuleState { - renderGroupManager: RenderGroupManager; - - constructor() { - this.renderGroupManager = new RenderGroupManager(); - } -} - -// To track the processing to be done between frames -export enum MousePointer { - NONE = -1, - LEFT = 0, - RIGHT = 2, - MIDDLE = 1, - OTHER = 7050, -} -export class FrameTracker { - private zoomTicks = 0; - - // Start off the first frame by initially zooming to fit - private zoomToFitOnce = true; - - private heldPointer: MousePointer = MousePointer.NONE; - - public lastX = -1; - - public lastY = -1; - - public rotateX = 0; - - public rotateY = 0; - - public panX = 0; - - public panY = 0; - - public getZoomTicks(): number { - return this.zoomTicks; - } - - public changeZoomTicks(wheelDelta: number) { - this.zoomTicks += Math.sign(wheelDelta); - } - - public setZoomToFit() { - this.zoomToFitOnce = true; - } - - public unsetLastCoordinates() { - this.lastX = -1; - this.lastY = -1; - } - - public setHeldPointer(mouseEventButton: number) { - switch (mouseEventButton) { - case MousePointer.LEFT: - case MousePointer.RIGHT: - case MousePointer.MIDDLE: - this.heldPointer = mouseEventButton; - break; - default: - this.heldPointer = MousePointer.OTHER; - break; - } - } - - public unsetHeldPointer() { - this.heldPointer = MousePointer.NONE; - } - - public shouldZoom(): boolean { - return this.zoomTicks !== 0; - } - - public didZoom() { - this.zoomTicks = 0; - } - - public shouldZoomToFit(): boolean { - return this.zoomToFitOnce; - } - - public didZoomToFit() { - this.zoomToFitOnce = false; - } - - public shouldRotate(): boolean { - return this.rotateX !== 0 || this.rotateY !== 0; - } - - public didRotate() { - this.rotateX = 0; - this.rotateY = 0; - } - - public shouldPan(): boolean { - return this.panX !== 0 || this.panY !== 0; - } - - public didPan() { - this.panX = 0; - this.panY = 0; - } - - public shouldIgnorePointerMove(): boolean { - return [MousePointer.NONE, MousePointer.RIGHT].includes(this.heldPointer); - } - - public isPointerPan(isShiftKey: boolean): boolean { - return ( - this.heldPointer === MousePointer.MIDDLE || - (this.heldPointer === MousePointer.LEFT && isShiftKey) - ); - } -} - -// Used as options when setting camera projection -export class CameraViewportDimensions { - public constructor(public width: number, public height: number) {} -} - -export function getModuleContext( - moduleContexts: ModuleContexts -): ModuleContext | null { - let potentialModuleContext: ModuleContext | undefined = moduleContexts.get( - 'csg' - ); - return potentialModuleContext ?? null; -} - -export function hexToColor(hex: string): Color { - let regex: RegExp = /^#?(?[\da-f]{2})(?[\da-f]{2})(?[\da-f]{2})$/iu; - let potentialGroups: { [key: string]: string } | undefined = hex.match(regex) - ?.groups; - if (potentialGroups === undefined) return [0, 0, 0]; - let groups: { [key: string]: string } = potentialGroups; - - return [ - parseInt(groups.red, 16) / 0xff, - parseInt(groups.green, 16) / 0xff, - parseInt(groups.blue, 16) / 0xff, - ]; -} - -export function colorToRgba(color: Color, opacity: number = 1): RGBA { - return [...color, opacity]; -} - -export function hexToRgba(hex: string): RGBA { - return colorToRgba(hexToColor(hex)); -} - -export function clamp(value: number, lowest: number, highest: number): number { - value = Math.max(value, lowest); - value = Math.min(value, highest); - return value; -} - -// When the object's class and the class used for comparison are from different -// contexts, they may appear identical, but are not recognised as such. -// This check acts as a useful yet not foolproof instanceof -export function looseInstanceof( - object: object | null | undefined, - c: any -): boolean { - const objectName: string | undefined = object?.constructor?.name; - const className: string | undefined = c?.name; - return ( - objectName !== undefined && - className !== undefined && - objectName === className - ); -} - -export function neatGridDistance(rawDistance: number) { - let paddedDistance: number = rawDistance + GRID_PADDING; - let roundedDistance: number = - Math.ceil(paddedDistance / ROUND_UP_INTERVAL) * ROUND_UP_INTERVAL; - return roundedDistance; -} +/* [Imports] */ +import { RGBA } from '@jscad/modeling/src/colors'; +import { clone, Geom3 } from '@jscad/modeling/src/geometries/geom3'; +import { + cameras, + controls as _controls, + drawCommands, + entitiesFromSolids as _entitiesFromSolids, + prepareRender as _prepareRender, +} from '@jscad/regl-renderer'; +import { ModuleContext, ModuleState } from 'js-slang'; +import { ModuleContexts } from '../../typings/type_helpers.js'; +import { + ACE_GUTTER_TEXT_COLOR, + BP_TEXT_COLOR, + GRID_PADDING, + MAIN_TICKS, + ROUND_UP_INTERVAL, + SUB_TICKS, +} from './constants.js'; +import { + AxisEntityType, + Color, + Controls, + ControlsState, + EntitiesFromSolids, + MultiGridEntityType, + PerspectiveCamera, + PerspectiveCameraState, + PrepareRender, + Solid, + WrappedRenderer, +} from './types'; + +/* [Exports] */ + +// [Proper typing for JS in regl-renderer] +export const perspectiveCamera: PerspectiveCamera = cameras.perspective; +export const perspectiveCameraStateDefaults: PerspectiveCameraState + = perspectiveCamera.defaults; + +export const controls: Controls = (_controls.orbit as unknown) as Controls; +export const controlsStateDefaults: ControlsState = controls.defaults; + +export const prepareRender: PrepareRender.Function = _prepareRender; + +export const entitiesFromSolids: EntitiesFromSolids.Function = (_entitiesFromSolids as unknown) as EntitiesFromSolids.Function; +export const prepareDrawCommands: WrappedRenderer.PrepareDrawCommands = drawCommands; + +// [Custom] +export class MultiGridEntity implements MultiGridEntityType { + visuals: { + drawCmd: 'drawGrid'; + show: boolean; + color?: RGBA; + subColor?: RGBA; + } = { + drawCmd: 'drawGrid', + show: true, + + color: hexToRgba(BP_TEXT_COLOR), + subColor: hexToRgba(ACE_GUTTER_TEXT_COLOR), + }; + + ticks: [number, number] = [MAIN_TICKS, SUB_TICKS]; + + size: [number, number]; + + constructor(size: number) { + this.size = [size, size]; + } +} + +export class AxisEntity implements AxisEntityType { + visuals: { + drawCmd: 'drawAxis'; + show: boolean; + } = { + drawCmd: 'drawAxis', + show: true, + }; + + alwaysVisible: boolean = false; + + constructor(public size?: number) {} +} + +export class Shape { + constructor(public solid: Solid) {} + + clone(): Shape { + return new Shape(clone(this.solid as Geom3)); + } + + toReplString(): string { + return ''; + } +} + +export class RenderGroup { + constructor(public canvasNumber: number) {} + + render: boolean = false; + hasGrid: boolean = true; + hasAxis: boolean = true; + + shapes: Shape[] = []; + + toReplString(): string { + return ``; + } +} + +export class RenderGroupManager { + private canvasTracker: number = 1; + private renderGroups: RenderGroup[] = []; + + constructor() { + this.addRenderGroup(); + } + + private addRenderGroup(): void { + // Passes in canvasTracker as is, then increments it + this.renderGroups.push(new RenderGroup(this.canvasTracker++)); + } + + private getCurrentRenderGroup(): RenderGroup { + return this.renderGroups.at(-1) as RenderGroup; + } + + nextRenderGroup( + currentGrid: boolean = false, + currentAxis: boolean = false, + ): RenderGroup { + let previousRenderGroup: RenderGroup = this.getCurrentRenderGroup(); + previousRenderGroup.render = true; + previousRenderGroup.hasGrid = currentGrid; + previousRenderGroup.hasAxis = currentAxis; + + this.addRenderGroup(); + + return previousRenderGroup; + } + + storeShape(shape: Shape): void { + this.getCurrentRenderGroup().shapes.push(shape); + } + + shouldRender(): boolean { + return this.getGroupsToRender().length > 0; + } + + getGroupsToRender(): RenderGroup[] { + return this.renderGroups.filter( + (renderGroup: RenderGroup) => renderGroup.render, + ); + } +} + +export class CsgModuleState implements ModuleState { + renderGroupManager: RenderGroupManager; + + constructor() { + this.renderGroupManager = new RenderGroupManager(); + } +} + +// To track the processing to be done between frames +export enum MousePointer { + NONE = -1, + LEFT = 0, + RIGHT = 2, + MIDDLE = 1, + OTHER = 7050, +} +export class FrameTracker { + private zoomTicks = 0; + + // Start off the first frame by initially zooming to fit + private zoomToFitOnce = true; + + private heldPointer: MousePointer = MousePointer.NONE; + + public lastX = -1; + + public lastY = -1; + + public rotateX = 0; + + public rotateY = 0; + + public panX = 0; + + public panY = 0; + + public getZoomTicks(): number { + return this.zoomTicks; + } + + public changeZoomTicks(wheelDelta: number) { + this.zoomTicks += Math.sign(wheelDelta); + } + + public setZoomToFit() { + this.zoomToFitOnce = true; + } + + public unsetLastCoordinates() { + this.lastX = -1; + this.lastY = -1; + } + + public setHeldPointer(mouseEventButton: number) { + switch (mouseEventButton) { + case MousePointer.LEFT: + case MousePointer.RIGHT: + case MousePointer.MIDDLE: + this.heldPointer = mouseEventButton; + break; + default: + this.heldPointer = MousePointer.OTHER; + break; + } + } + + public unsetHeldPointer() { + this.heldPointer = MousePointer.NONE; + } + + public shouldZoom(): boolean { + return this.zoomTicks !== 0; + } + + public didZoom() { + this.zoomTicks = 0; + } + + public shouldZoomToFit(): boolean { + return this.zoomToFitOnce; + } + + public didZoomToFit() { + this.zoomToFitOnce = false; + } + + public shouldRotate(): boolean { + return this.rotateX !== 0 || this.rotateY !== 0; + } + + public didRotate() { + this.rotateX = 0; + this.rotateY = 0; + } + + public shouldPan(): boolean { + return this.panX !== 0 || this.panY !== 0; + } + + public didPan() { + this.panX = 0; + this.panY = 0; + } + + public shouldIgnorePointerMove(): boolean { + return [MousePointer.NONE, MousePointer.RIGHT].includes(this.heldPointer); + } + + public isPointerPan(isShiftKey: boolean): boolean { + return ( + this.heldPointer === MousePointer.MIDDLE + || (this.heldPointer === MousePointer.LEFT && isShiftKey) + ); + } +} + +// Used as options when setting camera projection +export class CameraViewportDimensions { + public constructor(public width: number, public height: number) {} +} + +export function getModuleContext( + moduleContexts: ModuleContexts, +): ModuleContext | null { + let potentialModuleContext: ModuleContext | undefined = moduleContexts.get( + 'csg', + ); + return potentialModuleContext ?? null; +} + +export function hexToColor(hex: string): Color { + let regex: RegExp = /^#?(?[\da-f]{2})(?[\da-f]{2})(?[\da-f]{2})$/iu; + let potentialGroups: { [key: string]: string } | undefined = hex.match(regex) + ?.groups; + if (potentialGroups === undefined) return [0, 0, 0]; + let groups: { [key: string]: string } = potentialGroups; + + return [ + parseInt(groups.red, 16) / 0xff, + parseInt(groups.green, 16) / 0xff, + parseInt(groups.blue, 16) / 0xff, + ]; +} + +export function colorToRgba(color: Color, opacity: number = 1): RGBA { + return [...color, opacity]; +} + +export function hexToRgba(hex: string): RGBA { + return colorToRgba(hexToColor(hex)); +} + +export function clamp(value: number, lowest: number, highest: number): number { + value = Math.max(value, lowest); + value = Math.min(value, highest); + return value; +} + +// When the object's class and the class used for comparison are from different +// contexts, they may appear identical, but are not recognised as such. +// This check acts as a useful yet not foolproof instanceof +export function looseInstanceof( + object: object | null | undefined, + c: any, +): boolean { + const objectName: string | undefined = object?.constructor?.name; + const className: string | undefined = c?.name; + return ( + objectName !== undefined + && className !== undefined + && objectName === className + ); +} + +export function neatGridDistance(rawDistance: number) { + let paddedDistance: number = rawDistance + GRID_PADDING; + let roundedDistance: number + = Math.ceil(paddedDistance / ROUND_UP_INTERVAL) * ROUND_UP_INTERVAL; + return roundedDistance; +} diff --git a/src/bundles/curve/curves_webgl.ts b/src/bundles/curve/curves_webgl.ts index 0131dd480..015a02f16 100644 --- a/src/bundles/curve/curves_webgl.ts +++ b/src/bundles/curve/curves_webgl.ts @@ -1,457 +1,456 @@ -/* eslint-disable @typescript-eslint/naming-convention, complexity */ -import { mat4, vec3 } from 'gl-matrix'; -import { ReplResult } from '../../typings/type_helpers'; -import { CurveSpace, DrawMode, ScaleMode } from './types'; - -/** @hidden */ -export const drawnCurves: CurveDrawn[] = []; - -// Vertex shader program -const vsS: string = ` -attribute vec4 aFragColor; -attribute vec4 aVertexPosition; -uniform mat4 uModelViewMatrix; -uniform mat4 uProjectionMatrix; - -varying lowp vec4 aColor; - -void main() { - gl_PointSize = 2.0; - aColor = aFragColor; - gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; -}`; - -// Fragment shader program -const fsS: string = ` -varying lowp vec4 aColor; -precision mediump float; -void main() { - gl_FragColor = aColor; -}`; - -// ============================================================================= -// Module's Private Functions -// -// This file contains all the private functions used by the Curves module for -// rendering curves. For documentation/tutorials on WebGL API, see -// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API. -// ============================================================================= - -/** - * Gets shader based on given shader program code. - * - * @param gl - WebGL's rendering context - * @param type - constant describing the type of shader to load - * @param source - source code of the shader - * @returns WebGLShader used to initialize shader program - */ -function loadShader( - gl: WebGLRenderingContext, - type: number, - source: string -): WebGLShader { - const shader = gl.createShader(type); - if (!shader) { - throw new Error('WebGLShader not available.'); - } - gl.shaderSource(shader, source); - gl.compileShader(shader); - return shader; -} - -/** - * Initializes the shader program used by WebGL. - * - * @param gl - WebGL's rendering context - * @param vsSource - vertex shader program code - * @param fsSource - fragment shader program code - * @returns WebGLProgram used for getting AttribLocation and UniformLocation - */ -function initShaderProgram( - gl: WebGLRenderingContext, - vsSource: string, - fsSource: string -): WebGLProgram { - const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); - const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); - const shaderProgram = gl.createProgram(); - if (!shaderProgram) { - throw new Error('Unable to initialize the shader program.'); - } - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - return shaderProgram; -} - -/** - * The return type of the curve generated by the source code in workspace. - * WebGLCanvas in Curves tab captures the return value and calls its init function. - */ -type ProgramInfo = { - program: WebGLProgram; - attribLocations: { - vertexPosition: number; - vertexColor: number; - }; - uniformLocations: { - projectionMatrix: WebGLUniformLocation | null; - modelViewMatrix: WebGLUniformLocation | null; - }; -}; - -type BufferInfo = { - cubeBuffer: WebGLBuffer | null; - curveBuffer: WebGLBuffer | null; - curveColorBuffer: WebGLBuffer | null; -}; - -/** A function that takes in number from 0 to 1 and returns a Point. */ -export type Curve = (u: number) => Point; - -type Color = [r: number, g: number, b: number, t: number]; - -/** Encapsulates 3D point with RGB values. */ -export class Point implements ReplResult { - constructor( - public readonly x: number, - public readonly y: number, - public readonly z: number, - public readonly color: Color - ) {} - - public toReplString = () => - `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; -} - -/** - * Represents a Curve that has been generated from the `generateCurve` - * function. - */ -export class CurveDrawn implements ReplResult { - private renderingContext: WebGLRenderingContext | null; - - private programs: ProgramInfo | null; - - private buffersInfo: BufferInfo | null; - - constructor( - private readonly drawMode: DrawMode, - private readonly numPoints: number, - private readonly space: CurveSpace, - private readonly drawCubeArray: number[], - private readonly curvePosArray: number[], - private readonly curveColorArray: number[] - ) { - this.renderingContext = null; - this.programs = null; - this.buffersInfo = null; - } - - public toReplString = () => ''; - - public is3D = () => this.space === '3D'; - - public init = (canvas: HTMLCanvasElement) => { - this.renderingContext = canvas.getContext('webgl'); - if (!this.renderingContext) { - throw new Error('Rendering context cannot be null.'); - } - const cubeBuffer = this.renderingContext.createBuffer(); - this.renderingContext.bindBuffer( - this.renderingContext.ARRAY_BUFFER, - cubeBuffer - ); - this.renderingContext.bufferData( - this.renderingContext.ARRAY_BUFFER, - new Float32Array(this.drawCubeArray), - this.renderingContext.STATIC_DRAW - ); - - const curveBuffer = this.renderingContext.createBuffer(); - this.renderingContext.bindBuffer( - this.renderingContext.ARRAY_BUFFER, - curveBuffer - ); - this.renderingContext.bufferData( - this.renderingContext.ARRAY_BUFFER, - new Float32Array(this.curvePosArray), - this.renderingContext.STATIC_DRAW - ); - - const curveColorBuffer = this.renderingContext.createBuffer(); - this.renderingContext.bindBuffer( - this.renderingContext.ARRAY_BUFFER, - curveColorBuffer - ); - this.renderingContext.bufferData( - this.renderingContext.ARRAY_BUFFER, - new Float32Array(this.curveColorArray), - this.renderingContext.STATIC_DRAW - ); - - const shaderProgram = initShaderProgram(this.renderingContext, vsS, fsS); - this.programs = { - program: shaderProgram, - attribLocations: { - vertexPosition: this.renderingContext.getAttribLocation( - shaderProgram, - 'aVertexPosition' - ), - vertexColor: this.renderingContext.getAttribLocation( - shaderProgram, - 'aFragColor' - ), - }, - uniformLocations: { - projectionMatrix: this.renderingContext.getUniformLocation( - shaderProgram, - 'uProjectionMatrix' - ), - modelViewMatrix: this.renderingContext.getUniformLocation( - shaderProgram, - 'uModelViewMatrix' - ), - }, - }; - this.buffersInfo = { - cubeBuffer, - curveBuffer, - curveColorBuffer, - }; - }; - - public redraw = (angle: number) => { - if (!this.renderingContext) { - return; - } - - const gl = this.renderingContext; - const itemSize = this.space === '3D' ? 3 : 2; - gl.clearColor(1, 1, 1, 1); // Clear to white, fully opaque - gl.clearDepth(1.0); // Clear everything - gl.enable(gl.DEPTH_TEST); // Enable depth testing - gl.depthFunc(gl.LEQUAL); // Near things obscure far things - // eslint-disable-next-line no-bitwise - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - const transMat = mat4.create(); - const projMat = mat4.create(); - - if (this.space === '3D') { - const padding = Math.sqrt(1 / 3.1); - mat4.scale( - transMat, - transMat, - vec3.fromValues(padding, padding, padding) - ); - mat4.translate(transMat, transMat, [0, 0, -5]); - mat4.rotate(transMat, transMat, -(Math.PI / 2), [1, 0, 0]); // axis to rotate around X (static) - mat4.rotate(transMat, transMat, angle, [0, 0, 1]); // axis to rotate around Z (dynamic) - - const fieldOfView = (45 * Math.PI) / 180; - const aspect = gl.canvas.width / gl.canvas.height; - const zNear = 0.01; // Must not be zero, depth testing loses precision proportional to log(zFar / zNear) - const zFar = 50.0; - mat4.perspective(projMat, fieldOfView, aspect, zNear, zFar); - } - - gl.useProgram(this.programs!.program); - gl.uniformMatrix4fv( - this.programs!.uniformLocations.projectionMatrix, - false, - projMat - ); - gl.uniformMatrix4fv( - this.programs!.uniformLocations.modelViewMatrix, - false, - transMat - ); - gl.enableVertexAttribArray(this.programs!.attribLocations.vertexPosition); - gl.enableVertexAttribArray(this.programs!.attribLocations.vertexColor); - - if (this.space === '3D') { - // Draw Cube - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.cubeBuffer); - gl.vertexAttribPointer( - this.programs!.attribLocations.vertexPosition, - 3, - gl.FLOAT, - false, - 0, - 0 - ); - const colors: number[] = []; - for (let i = 0; i < 16; i += 1) { - colors.push(0.6, 0.6, 0.6, 1); - } - const colorBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); - gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0); - gl.drawArrays(gl.LINE_STRIP, 0, 16); - } - // Draw Curve - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.curveBuffer); - gl.vertexAttribPointer( - this.programs!.attribLocations.vertexPosition, - itemSize, - gl.FLOAT, - false, - 0, - 0 - ); - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.curveColorBuffer); - gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0); - if (this.drawMode === 'lines') { - gl.drawArrays(gl.LINE_STRIP, 0, this.numPoints + 1); - } else { - gl.drawArrays(gl.POINTS, 0, this.numPoints + 1); - } - }; -} - -export function generateCurve( - scaleMode: ScaleMode, - drawMode: DrawMode, - numPoints: number, - func: Curve, - space: CurveSpace, - isFullView: boolean -) { - const curvePosArray: number[] = []; - const curveColorArray: number[] = []; - const drawCubeArray: number[] = []; - - // initialize the min/max to extreme values - let min_x = Infinity; - let max_x = -Infinity; - let min_y = Infinity; - let max_y = -Infinity; - let min_z = Infinity; - let max_z = -Infinity; - - for (let i = 0; i <= numPoints; i += 1) { - const point = func(i / numPoints); - const x = point.x * 2 - 1; - const y = point.y * 2 - 1; - const z = point.z * 2 - 1; - if (space === '2D') { - curvePosArray.push(x, y); - } else { - curvePosArray.push(x, y, z); - } - const color_r = point.color[0]; - const color_g = point.color[1]; - const color_b = point.color[2]; - const color_a = point.color[3]; - curveColorArray.push(color_r, color_g, color_b, color_a); - min_x = Math.min(min_x, x); - max_x = Math.max(max_x, x); - min_y = Math.min(min_y, y); - max_y = Math.max(max_y, y); - min_z = Math.min(min_z, z); - max_z = Math.max(max_z, z); - } - - // padding for 2d draw_connected_full_view - if (isFullView) { - const horiz_padding = 0.05 * (max_x - min_x); - min_x -= horiz_padding; - max_x += horiz_padding; - const vert_padding = 0.05 * (max_y - min_y); - min_y -= vert_padding; - max_y += vert_padding; - const depth_padding = 0.05 * (max_z - min_z); - min_z -= depth_padding; - max_z += depth_padding; - } - - // box generation, coordinates are added into the array using 4 push - // operations to improve on readability during code editing. - if (space === '3D') { - drawCubeArray.push(-1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1); - drawCubeArray.push(1, 1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1); - drawCubeArray.push(1, -1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1); - drawCubeArray.push(-1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1); - } else { - min_z = 0; - max_z = 0; - } - - if (scaleMode === 'fit') { - const center = [ - (min_x + max_x) / 2, - (min_y + max_y) / 2, - (min_z + max_z) / 2, - ]; - let scale = Math.max(max_x - min_x, max_y - min_y, max_z - min_z); - scale = scale === 0 ? 1 : scale; - if (space === '3D') { - for (let i = 0; i < curvePosArray.length; i += 1) { - if (i % 3 === 0) { - curvePosArray[i] -= center[0]; - curvePosArray[i] /= scale / 2; - } else if (i % 3 === 1) { - curvePosArray[i] -= center[1]; - curvePosArray[i] /= scale / 2; - } else { - curvePosArray[i] -= center[2]; - curvePosArray[i] /= scale / 2; - } - } - } else { - for (let i = 0; i < curvePosArray.length; i += 1) { - if (i % 2 === 0) { - curvePosArray[i] -= center[0]; - curvePosArray[i] /= scale / 2; - } else { - curvePosArray[i] -= center[1]; - curvePosArray[i] /= scale / 2; - } - } - } - } else if (scaleMode === 'stretch') { - const center = [ - (min_x + max_x) / 2, - (min_y + max_y) / 2, - (min_z + max_z) / 2, - ]; - const x_scale = max_x === min_x ? 1 : max_x - min_x; - const y_scale = max_y === min_y ? 1 : max_y - min_y; - const z_scale = max_z === min_z ? 1 : max_z - min_z; - if (space === '3D') { - for (let i = 0; i < curvePosArray.length; i += 1) { - if (i % 3 === 0) { - curvePosArray[i] -= center[0]; - curvePosArray[i] /= x_scale / 2; - } else if (i % 3 === 1) { - curvePosArray[i] -= center[1]; - curvePosArray[i] /= y_scale / 2; - } else { - curvePosArray[i] -= center[2]; - curvePosArray[i] /= z_scale / 2; - } - } - } else { - for (let i = 0; i < curvePosArray.length; i += 1) { - if (i % 2 === 0) { - curvePosArray[i] -= center[0]; - curvePosArray[i] /= x_scale / 2; - } else { - curvePosArray[i] -= center[1]; - curvePosArray[i] /= y_scale / 2; - } - } - } - } - - return new CurveDrawn( - drawMode, - numPoints, - space, - drawCubeArray, - curvePosArray, - curveColorArray - ); -} +/* eslint-disable @typescript-eslint/naming-convention, complexity */ +import { mat4, vec3 } from 'gl-matrix'; +import { ReplResult } from '../../typings/type_helpers'; +import { CurveSpace, DrawMode, ScaleMode } from './types'; + +/** @hidden */ +export const drawnCurves: CurveDrawn[] = []; + +// Vertex shader program +const vsS: string = ` +attribute vec4 aFragColor; +attribute vec4 aVertexPosition; +uniform mat4 uModelViewMatrix; +uniform mat4 uProjectionMatrix; + +varying lowp vec4 aColor; + +void main() { + gl_PointSize = 2.0; + aColor = aFragColor; + gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; +}`; + +// Fragment shader program +const fsS: string = ` +varying lowp vec4 aColor; +precision mediump float; +void main() { + gl_FragColor = aColor; +}`; + +// ============================================================================= +// Module's Private Functions +// +// This file contains all the private functions used by the Curves module for +// rendering curves. For documentation/tutorials on WebGL API, see +// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API. +// ============================================================================= + +/** + * Gets shader based on given shader program code. + * + * @param gl - WebGL's rendering context + * @param type - constant describing the type of shader to load + * @param source - source code of the shader + * @returns WebGLShader used to initialize shader program + */ +function loadShader( + gl: WebGLRenderingContext, + type: number, + source: string, +): WebGLShader { + const shader = gl.createShader(type); + if (!shader) { + throw new Error('WebGLShader not available.'); + } + gl.shaderSource(shader, source); + gl.compileShader(shader); + return shader; +} + +/** + * Initializes the shader program used by WebGL. + * + * @param gl - WebGL's rendering context + * @param vsSource - vertex shader program code + * @param fsSource - fragment shader program code + * @returns WebGLProgram used for getting AttribLocation and UniformLocation + */ +function initShaderProgram( + gl: WebGLRenderingContext, + vsSource: string, + fsSource: string, +): WebGLProgram { + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); + const shaderProgram = gl.createProgram(); + if (!shaderProgram) { + throw new Error('Unable to initialize the shader program.'); + } + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + return shaderProgram; +} + +/** + * The return type of the curve generated by the source code in workspace. + * WebGLCanvas in Curves tab captures the return value and calls its init function. + */ +type ProgramInfo = { + program: WebGLProgram; + attribLocations: { + vertexPosition: number; + vertexColor: number; + }; + uniformLocations: { + projectionMatrix: WebGLUniformLocation | null; + modelViewMatrix: WebGLUniformLocation | null; + }; +}; + +type BufferInfo = { + cubeBuffer: WebGLBuffer | null; + curveBuffer: WebGLBuffer | null; + curveColorBuffer: WebGLBuffer | null; +}; + +/** A function that takes in number from 0 to 1 and returns a Point. */ +export type Curve = (u: number) => Point; + +type Color = [r: number, g: number, b: number, t: number]; + +/** Encapsulates 3D point with RGB values. */ +export class Point implements ReplResult { + constructor( + public readonly x: number, + public readonly y: number, + public readonly z: number, + public readonly color: Color, + ) {} + + public toReplString = () => `(${this.x}, ${this.y}, ${this.z}, Color: ${this.color})`; +} + +/** + * Represents a Curve that has been generated from the `generateCurve` + * function. + */ +export class CurveDrawn implements ReplResult { + private renderingContext: WebGLRenderingContext | null; + + private programs: ProgramInfo | null; + + private buffersInfo: BufferInfo | null; + + constructor( + private readonly drawMode: DrawMode, + private readonly numPoints: number, + private readonly space: CurveSpace, + private readonly drawCubeArray: number[], + private readonly curvePosArray: number[], + private readonly curveColorArray: number[], + ) { + this.renderingContext = null; + this.programs = null; + this.buffersInfo = null; + } + + public toReplString = () => ''; + + public is3D = () => this.space === '3D'; + + public init = (canvas: HTMLCanvasElement) => { + this.renderingContext = canvas.getContext('webgl'); + if (!this.renderingContext) { + throw new Error('Rendering context cannot be null.'); + } + const cubeBuffer = this.renderingContext.createBuffer(); + this.renderingContext.bindBuffer( + this.renderingContext.ARRAY_BUFFER, + cubeBuffer, + ); + this.renderingContext.bufferData( + this.renderingContext.ARRAY_BUFFER, + new Float32Array(this.drawCubeArray), + this.renderingContext.STATIC_DRAW, + ); + + const curveBuffer = this.renderingContext.createBuffer(); + this.renderingContext.bindBuffer( + this.renderingContext.ARRAY_BUFFER, + curveBuffer, + ); + this.renderingContext.bufferData( + this.renderingContext.ARRAY_BUFFER, + new Float32Array(this.curvePosArray), + this.renderingContext.STATIC_DRAW, + ); + + const curveColorBuffer = this.renderingContext.createBuffer(); + this.renderingContext.bindBuffer( + this.renderingContext.ARRAY_BUFFER, + curveColorBuffer, + ); + this.renderingContext.bufferData( + this.renderingContext.ARRAY_BUFFER, + new Float32Array(this.curveColorArray), + this.renderingContext.STATIC_DRAW, + ); + + const shaderProgram = initShaderProgram(this.renderingContext, vsS, fsS); + this.programs = { + program: shaderProgram, + attribLocations: { + vertexPosition: this.renderingContext.getAttribLocation( + shaderProgram, + 'aVertexPosition', + ), + vertexColor: this.renderingContext.getAttribLocation( + shaderProgram, + 'aFragColor', + ), + }, + uniformLocations: { + projectionMatrix: this.renderingContext.getUniformLocation( + shaderProgram, + 'uProjectionMatrix', + ), + modelViewMatrix: this.renderingContext.getUniformLocation( + shaderProgram, + 'uModelViewMatrix', + ), + }, + }; + this.buffersInfo = { + cubeBuffer, + curveBuffer, + curveColorBuffer, + }; + }; + + public redraw = (angle: number) => { + if (!this.renderingContext) { + return; + } + + const gl = this.renderingContext; + const itemSize = this.space === '3D' ? 3 : 2; + gl.clearColor(1, 1, 1, 1); // Clear to white, fully opaque + gl.clearDepth(1.0); // Clear everything + gl.enable(gl.DEPTH_TEST); // Enable depth testing + gl.depthFunc(gl.LEQUAL); // Near things obscure far things + // eslint-disable-next-line no-bitwise + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const transMat = mat4.create(); + const projMat = mat4.create(); + + if (this.space === '3D') { + const padding = Math.sqrt(1 / 3.1); + mat4.scale( + transMat, + transMat, + vec3.fromValues(padding, padding, padding), + ); + mat4.translate(transMat, transMat, [0, 0, -5]); + mat4.rotate(transMat, transMat, -(Math.PI / 2), [1, 0, 0]); // axis to rotate around X (static) + mat4.rotate(transMat, transMat, angle, [0, 0, 1]); // axis to rotate around Z (dynamic) + + const fieldOfView = (45 * Math.PI) / 180; + const aspect = gl.canvas.width / gl.canvas.height; + const zNear = 0.01; // Must not be zero, depth testing loses precision proportional to log(zFar / zNear) + const zFar = 50.0; + mat4.perspective(projMat, fieldOfView, aspect, zNear, zFar); + } + + gl.useProgram(this.programs!.program); + gl.uniformMatrix4fv( + this.programs!.uniformLocations.projectionMatrix, + false, + projMat, + ); + gl.uniformMatrix4fv( + this.programs!.uniformLocations.modelViewMatrix, + false, + transMat, + ); + gl.enableVertexAttribArray(this.programs!.attribLocations.vertexPosition); + gl.enableVertexAttribArray(this.programs!.attribLocations.vertexColor); + + if (this.space === '3D') { + // Draw Cube + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.cubeBuffer); + gl.vertexAttribPointer( + this.programs!.attribLocations.vertexPosition, + 3, + gl.FLOAT, + false, + 0, + 0, + ); + const colors: number[] = []; + for (let i = 0; i < 16; i += 1) { + colors.push(0.6, 0.6, 0.6, 1); + } + const colorBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0); + gl.drawArrays(gl.LINE_STRIP, 0, 16); + } + // Draw Curve + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.curveBuffer); + gl.vertexAttribPointer( + this.programs!.attribLocations.vertexPosition, + itemSize, + gl.FLOAT, + false, + 0, + 0, + ); + gl.bindBuffer(gl.ARRAY_BUFFER, this.buffersInfo!.curveColorBuffer); + gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0); + if (this.drawMode === 'lines') { + gl.drawArrays(gl.LINE_STRIP, 0, this.numPoints + 1); + } else { + gl.drawArrays(gl.POINTS, 0, this.numPoints + 1); + } + }; +} + +export function generateCurve( + scaleMode: ScaleMode, + drawMode: DrawMode, + numPoints: number, + func: Curve, + space: CurveSpace, + isFullView: boolean, +) { + const curvePosArray: number[] = []; + const curveColorArray: number[] = []; + const drawCubeArray: number[] = []; + + // initialize the min/max to extreme values + let min_x = Infinity; + let max_x = -Infinity; + let min_y = Infinity; + let max_y = -Infinity; + let min_z = Infinity; + let max_z = -Infinity; + + for (let i = 0; i <= numPoints; i += 1) { + const point = func(i / numPoints); + const x = point.x * 2 - 1; + const y = point.y * 2 - 1; + const z = point.z * 2 - 1; + if (space === '2D') { + curvePosArray.push(x, y); + } else { + curvePosArray.push(x, y, z); + } + const color_r = point.color[0]; + const color_g = point.color[1]; + const color_b = point.color[2]; + const color_a = point.color[3]; + curveColorArray.push(color_r, color_g, color_b, color_a); + min_x = Math.min(min_x, x); + max_x = Math.max(max_x, x); + min_y = Math.min(min_y, y); + max_y = Math.max(max_y, y); + min_z = Math.min(min_z, z); + max_z = Math.max(max_z, z); + } + + // padding for 2d draw_connected_full_view + if (isFullView) { + const horiz_padding = 0.05 * (max_x - min_x); + min_x -= horiz_padding; + max_x += horiz_padding; + const vert_padding = 0.05 * (max_y - min_y); + min_y -= vert_padding; + max_y += vert_padding; + const depth_padding = 0.05 * (max_z - min_z); + min_z -= depth_padding; + max_z += depth_padding; + } + + // box generation, coordinates are added into the array using 4 push + // operations to improve on readability during code editing. + if (space === '3D') { + drawCubeArray.push(-1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1); + drawCubeArray.push(1, 1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1); + drawCubeArray.push(1, -1, 1, -1, -1, 1, 1, -1, 1, 1, 1, 1); + drawCubeArray.push(-1, 1, 1, -1, 1, -1, 1, 1, -1, 1, 1, 1); + } else { + min_z = 0; + max_z = 0; + } + + if (scaleMode === 'fit') { + const center = [ + (min_x + max_x) / 2, + (min_y + max_y) / 2, + (min_z + max_z) / 2, + ]; + let scale = Math.max(max_x - min_x, max_y - min_y, max_z - min_z); + scale = scale === 0 ? 1 : scale; + if (space === '3D') { + for (let i = 0; i < curvePosArray.length; i += 1) { + if (i % 3 === 0) { + curvePosArray[i] -= center[0]; + curvePosArray[i] /= scale / 2; + } else if (i % 3 === 1) { + curvePosArray[i] -= center[1]; + curvePosArray[i] /= scale / 2; + } else { + curvePosArray[i] -= center[2]; + curvePosArray[i] /= scale / 2; + } + } + } else { + for (let i = 0; i < curvePosArray.length; i += 1) { + if (i % 2 === 0) { + curvePosArray[i] -= center[0]; + curvePosArray[i] /= scale / 2; + } else { + curvePosArray[i] -= center[1]; + curvePosArray[i] /= scale / 2; + } + } + } + } else if (scaleMode === 'stretch') { + const center = [ + (min_x + max_x) / 2, + (min_y + max_y) / 2, + (min_z + max_z) / 2, + ]; + const x_scale = max_x === min_x ? 1 : max_x - min_x; + const y_scale = max_y === min_y ? 1 : max_y - min_y; + const z_scale = max_z === min_z ? 1 : max_z - min_z; + if (space === '3D') { + for (let i = 0; i < curvePosArray.length; i += 1) { + if (i % 3 === 0) { + curvePosArray[i] -= center[0]; + curvePosArray[i] /= x_scale / 2; + } else if (i % 3 === 1) { + curvePosArray[i] -= center[1]; + curvePosArray[i] /= y_scale / 2; + } else { + curvePosArray[i] -= center[2]; + curvePosArray[i] /= z_scale / 2; + } + } + } else { + for (let i = 0; i < curvePosArray.length; i += 1) { + if (i % 2 === 0) { + curvePosArray[i] -= center[0]; + curvePosArray[i] /= x_scale / 2; + } else { + curvePosArray[i] -= center[1]; + curvePosArray[i] /= y_scale / 2; + } + } + } + } + + return new CurveDrawn( + drawMode, + numPoints, + space, + drawCubeArray, + curvePosArray, + curveColorArray, + ); +} diff --git a/src/bundles/curve/functions.ts b/src/bundles/curve/functions.ts index 206d9dcb1..a23f77879 100644 --- a/src/bundles/curve/functions.ts +++ b/src/bundles/curve/functions.ts @@ -1,843 +1,845 @@ -/** - * drawing *curves*, i.e. collections of *points*, on a canvas in a tools tab - * - * A *point* is defined by its coordinates (x, y and z), and the color assigned to - * it (r, g, and b). A few constructors for points is given, for example - * `make_color_point`. Selectors allow access to the coordinates and color - * components, for example `x_of`. - * - * A *curve* is a - * unary function which takes a number argument within the unit interval `[0,1]` - * and returns a point. If `C` is a curve, then the starting point of the curve - * is always `C(0)`, and the ending point is always `C(1)`. - * - * A *curve transformation* is a function that takes a curve as argument and - * returns a curve. Examples of curve transformations are `scale` and `translate`. - * - * A *curve drawer* is function that takes a number argument and returns - * a function that takes a curve as argument and visualises it in the output screen is - * shown in the Source Academy in the tab with the "Curves Canvas" icon (image). - * The following [example](https://share.sourceacademy.org/unitcircle) uses - * the curve drawer `draw_connected_full_view` to display a curve called - * `unit_circle`. - * ``` - * import { make_point, draw_connected_full_view } from "curve"; - * function unit_circle(t) { - * return make_point(math_sin(2 * math_PI * t), - * math_cos(2 * math_PI * t)); - * } - * draw_connected_full_view(100)(unit_circle); - * ``` - * draws a full circle in the display tab. - * - * @module curve - * @author Lee Zheng Han - * @author Ng Yong Xiang - */ - -/* eslint-disable @typescript-eslint/naming-convention */ -import { Curve, CurveDrawn, generateCurve, Point } from './curves_webgl'; -import { - AnimatedCurve, - CurveAnimation, - CurveSpace, - CurveTransformer, - DrawMode, - RenderFunction, - ScaleMode, -} from './types'; - -/** @hidden */ -export const drawnCurves: (CurveDrawn | AnimatedCurve)[] = []; - -function createDrawFunction( - scaleMode: ScaleMode, - drawMode: DrawMode, - space: CurveSpace, - isFullView: boolean -): (numPoints: number) => RenderFunction { - return (numPoints: number) => { - const func = (curve) => { - const curveDrawn = generateCurve( - scaleMode, - drawMode, - numPoints, - curve, - space, - isFullView - ); - - if ( - (curve as any).shouldAppend === undefined || - (curve as any).shouldAppend - ) { - drawnCurves.push(curveDrawn); - } - - return curveDrawn; - }; - // Because the draw functions are actually functions - // we need hacky workarounds like these to pass information around - func.is3D = space === '3D'; - return func; - }; -} - -// ============================================================================= -// Module's Exposed Functions -// -// This file only includes the implementation and documentation of exposed -// functions of the module. For private functions dealing with the browser's -// graphics library context, see './curves_webgl.ts'. -// ============================================================================= - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points and connecting each pair with a line. - * The parts between (0,0) and (1,1) of the resulting Drawing are shown in the window. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_connected(100)(t => make_point(t, t)); - * ``` - */ -export const draw_connected = createDrawFunction('none', 'lines', '2D', false); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points and connecting each pair with a line. The Drawing is - * translated and stretched/shrunk to show the full curve and maximize its width - * and height, with some padding. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_connected_full_view(100)(t => make_point(t, t)); - * ``` - */ -export const draw_connected_full_view = createDrawFunction( - 'stretch', - 'lines', - '2D', - true -); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points and connecting each pair with a line. The Drawing - * is translated and scaled proportionally to show the full curve and maximize - * its size, with some padding. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_connected_full_view_proportional(100)(t => make_point(t, t)); - * ``` - */ -export const draw_connected_full_view_proportional = createDrawFunction( - 'fit', - 'lines', - '2D', - true -); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points. The Drawing consists of isolated - * points, and does not connect them. The parts between (0,0) and (1,1) of the - * resulting Drawing are shown in the window. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1,there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_points(100)(t => make_point(t, t)); - * ``` - */ -export const draw_points = createDrawFunction('none', 'points', '2D', false); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points. The Drawing consists of isolated - * points, and does not connect them. The Drawing is translated and - * stretched/shrunk to show the full curve and maximize its width and height, - * with some padding. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_points_full_view(100)(t => make_point(t, t)); - * ``` - */ -export const draw_points_full_view = createDrawFunction( - 'stretch', - 'points', - '2D', - true -); - -/** - * Returns a function that turns a given Curve into a Drawing, by sampling the - * Curve at `num` sample points. The Drawing consists of isolated - * points, and does not connect them. The Drawing is translated and scaled - * proportionally with its size maximized to fit entirely inside the window, - * with some padding. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_points_full_view_proportional(100)(t => make_point(t, t)); - * ``` - */ -export const draw_points_full_view_proportional = createDrawFunction( - 'fit', - 'points', - '2D', - true -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points and connecting each pair with - * a line. The parts between (0,0,0) and (1,1,1) of the resulting Drawing are - * shown within the unit cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_connected(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_connected = createDrawFunction( - 'none', - 'lines', - '3D', - false -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points and connecting each pair with - * a line. The Drawing is translated and stretched/shrunk to show the full - * curve and maximize its width and height within the cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_connected_full_view(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_connected_full_view = createDrawFunction( - 'stretch', - 'lines', - '3D', - false -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points and connecting each pair with - * a line. The Drawing is translated and scaled proportionally with its size - * maximized to fit entirely inside the cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_connected_full_view_proportional(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_connected_full_view_proportional = createDrawFunction( - 'fit', - 'lines', - '3D', - false -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points. The Drawing consists of - * isolated points, and does not connect them. The parts between (0,0,0) - * and (1,1,1) of the resulting Drawing are shown within the unit cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_points(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_points = createDrawFunction('none', 'points', '3D', false); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points. The Drawing consists of - * isolated points, and does not connect them. The Drawing is translated and - * stretched/shrunk to maximize its size to fit entirely inside the cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_points_full_view(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_points_full_view = createDrawFunction( - 'stretch', - 'points', - '3D', - false -); - -/** - * Returns a function that turns a given 3D Curve into a Drawing, by sampling - * the 3D Curve at `num` sample points. The Drawing consists of - * isolated points, and does not connect them. The Drawing is translated and - * scaled proportionally with its size maximized to fit entirely inside the cube. - * - * @param num determines the number of points, lower than 65535, to be sampled. - * Including 0 and 1, there are `num + 1` evenly spaced sample points - * @return function of type Curve → Drawing - * @example - * ``` - * draw_3D_points_full_view_proportional(100)(t => make_3D_point(t, t, t)); - * ``` - */ -export const draw_3D_points_full_view_proportional = createDrawFunction( - 'fit', - 'points', - '3D', - false -); - -/** - * Makes a Point with given x and y coordinates. - * - * @param x x-coordinate of new point - * @param y y-coordinate of new point - * @returns with x and y as coordinates - * @example - * ``` - * const point = make_point(0.5, 0.5); - * ``` - */ -export function make_point(x: number, y: number): Point { - return new Point(x, y, 0, [0, 0, 0, 1]); -} - -/** - * Makes a 3D Point with given x, y and z coordinates. - * - * @param x x-coordinate of new point - * @param y y-coordinate of new point - * @param z z-coordinate of new point - * @returns with x, y and z as coordinates - * @example - * ``` - * const point = make_3D_point(0.5, 0.5, 0.5); - * ``` - */ -export function make_3D_point(x: number, y: number, z: number): Point { - return new Point(x, y, z, [0, 0, 0, 1]); -} - -/** - * Makes a color Point with given x and y coordinates, and RGB values ranging - * from 0 to 255. Any input lower than 0 for RGB will be rounded up to 0, and - * any input higher than 255 will be rounded down to 255. - * - * @param x x-coordinate of new point - * @param y y-coordinate of new point - * @param r red component of new point - * @param g green component of new point - * @param b blue component of new point - * @returns with x and y as coordinates, and r, g and b as RGB values - * @example - * ``` - * const redPoint = make_color_point(0.5, 0.5, 255, 0, 0); - * ``` - */ -export function make_color_point( - x: number, - y: number, - r: number, - g: number, - b: number -): Point { - return new Point(x, y, 0, [r / 255, g / 255, b / 255, 1]); -} - -/** - * Makes a 3D color Point with given x, y and z coordinates, and RGB values - * ranging from 0 to 255. Any input lower than 0 for RGB will be rounded up to - * 0, and any input higher than 255 will be rounded down to 255. - * - * @param x x-coordinate of new point - * @param y y-coordinate of new point - * @param z z-coordinate of new point - * @param r red component of new point - * @param g green component of new point - * @param b blue component of new point - * @returns with x, y and z as coordinates, and r, g and b as RGB values - * @example - * ``` - * const redPoint = make_color_point(0.5, 0.5, 0.5, 255, 0, 0); - * ``` - */ -export function make_3D_color_point( - x: number, - y: number, - z: number, - r: number, - g: number, - b: number -): Point { - return new Point(x, y, z, [r / 255, g / 255, b / 255, 1]); -} - -/** - * Retrieves the x-coordinate of a given Point. - * - * @param p given point - * @returns x-coordinate of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * x_of(point); // Returns 1 - * ``` - */ -export function x_of(pt: Point): number { - return pt.x; -} - -/** - * Retrieves the y-coordinate of a given Point. - * - * @param p given point - * @returns y-coordinate of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * y_of(point); // Returns 2 - * ``` - */ -export function y_of(pt: Point): number { - return pt.y; -} - -/** - * Retrieves the z-coordinate of a given Point. - * - * @param p given point - * @returns z-coordinate of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * z_of(point); // Returns 3 - * ``` - */ -export function z_of(pt: Point): number { - return pt.z; -} - -/** - * Retrieves the red component of a given Point. - * - * @param p given point - * @returns Red component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * r_of(point); // Returns 50 - * ``` - */ -export function r_of(pt: Point): number { - return pt.color[0] * 255; -} - -/** - * Retrieves the green component of a given Point. - * - * @param p given point - * @returns Green component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * g_of(point); // Returns 100 - * ``` - */ -export function g_of(pt: Point): number { - return pt.color[1] * 255; -} - -/** - * Retrieves the blue component of a given Point. - * - * @param p given point - * @returns Blue component of the Point - * @example - * ``` - * const point = make_color_point(1, 2, 3, 50, 100, 150); - * b_of(point); // Returns 150 - * ``` - */ -export function b_of(pt: Point): number { - return pt.color[2] * 255; -} - -/** - * This function is a Curve transformation: a function from a Curve to a Curve. - * The points of the result Curve are the same points as the points of the - * original Curve, but in reverse: The result Curve applied to 0 is the original - * Curve applied to 1 and vice versa. - * - * @param original original Curve - * @returns result Curve - */ -export function invert(curve: Curve): Curve { - return (t: number) => curve(1 - t); -} - -/** - * This function returns a Curve transformation: It takes an x-value x0, a - * y-value y0 and a z-value z0, as arguments and - * returns a Curve transformation that takes a Curve as argument and returns a - * new Curve, by translating the original by x0 in x-direction, y0 in - * y-direction and z0 in z-direction. - * - * @param x0 x-value - * @param y0 y-value - * @param z0 z-value - * @returns Curve transformation - */ -export function translate( - x0: number, - y0: number, - z0: number -): CurveTransformer { - return (curve: Curve) => { - const transformation = (cf: Curve) => (t: number) => { - const a = x0 === undefined ? 0 : x0; - const b = y0 === undefined ? 0 : y0; - const c = z0 === undefined ? 0 : z0; - const ct: Point = cf(t); - return make_3D_color_point( - a + x_of(ct), - b + y_of(ct), - c + z_of(ct), - r_of(ct), - g_of(ct), - b_of(ct) - ); - }; - return transformation(curve); - }; -} - -/** - * This function takes 3 angles, a, b and c in radians as parameter - * and returns a Curve transformation: a function that takes a Curve as argument - * and returns a new Curve, which is the original Curve rotated - * extrinsically with Euler angles (a, b, c) about x, y, - * and z axes. - * - * @param a given angle - * @param b given angle - * @param c given angle - * @returns function that takes a Curve and returns a Curve - */ -export function rotate_around_origin( - theta1: number, - theta2: number, - theta3: number -): CurveTransformer { - if (theta3 === undefined && theta1 !== undefined && theta2 !== undefined) { - // 2 args - throw new Error('Expected 1 or 3 arguments, but received 2'); - } else if ( - theta1 !== undefined && - theta2 === undefined && - theta3 === undefined - ) { - // 1 args - const cth = Math.cos(theta1); - const sth = Math.sin(theta1); - return (curve: Curve) => { - const transformation = (c: Curve) => (t: number) => { - const ct = c(t); - const x = x_of(ct); - const y = y_of(ct); - const z = z_of(ct); - return make_3D_color_point( - cth * x - sth * y, - sth * x + cth * y, - z, - r_of(ct), - g_of(ct), - b_of(ct) - ); - }; - return transformation(curve); - }; - } else { - const cthx = Math.cos(theta1); - const sthx = Math.sin(theta1); - const cthy = Math.cos(theta2); - const sthy = Math.sin(theta2); - const cthz = Math.cos(theta3); - const sthz = Math.sin(theta3); - return (curve: Curve) => { - const transformation = (c: Curve) => (t: number) => { - const ct = c(t); - const coord = [x_of(ct), y_of(ct), z_of(ct)]; - const mat = [ - [ - cthz * cthy, - cthz * sthy * sthx - sthz * cthx, - cthz * sthy * cthx + sthz * sthx, - ], - [ - sthz * cthy, - sthz * sthy * sthx + cthz * cthx, - sthz * sthy * cthx - cthz * sthx, - ], - [-sthy, cthy * sthx, cthy * cthx], - ]; - let xf = 0; - let yf = 0; - let zf = 0; - for (let i = 0; i < 3; i += 1) { - xf += mat[0][i] * coord[i]; - yf += mat[1][i] * coord[i]; - zf += mat[2][i] * coord[i]; - } - return make_3D_color_point(xf, yf, zf, r_of(ct), g_of(ct), b_of(ct)); - }; - return transformation(curve); - }; - } -} - -/** - * This function takes scaling factors `a`, `b` and - * `c`, as arguments and returns a - * Curve transformation that scales a given Curve by `a` in - * x-direction, `b` in y-direction and `c` in z-direction. - * - * @param a scaling factor in x-direction - * @param b scaling factor in y-direction - * @param c scaling factor in z-direction - * @returns function that takes a Curve and returns a Curve - */ -export function scale(a: number, b: number, c: number): CurveTransformer { - return (curve) => { - const transformation = (cf: Curve) => (t: number) => { - const ct = cf(t); - const a1 = a === undefined ? 1 : a; - const b1 = b === undefined ? 1 : b; - const c1 = c === undefined ? 1 : c; - return make_3D_color_point( - a1 * x_of(ct), - b1 * y_of(ct), - c1 * z_of(ct), - r_of(ct), - g_of(ct), - b_of(ct) - ); - }; - return transformation(curve); - }; -} - -/** - * This function takes a scaling factor s argument and returns a Curve - * transformation that scales a given Curve by s in x, y and z direction. - * - * @param s scaling factor - * @returns function that takes a Curve and returns a Curve - */ -export function scale_proportional(s: number): CurveTransformer { - return scale(s, s, s); -} - -/** - * This function is a Curve transformation: It takes a Curve as argument and - * returns a new Curve, as follows. A Curve is in standard position if it - * starts at (0,0) ends at (1,0). This function puts the given Curve in - * standard position by rigidly translating it so its start Point is at the - * origin (0,0), then rotating it about the origin to put its endpoint on the - * x axis, then scaling it to put the endpoint at (1,0). Behavior is unspecified - * on closed Curves where start-point equal end-point. - * - * @param curve given Curve - * @returns result Curve - */ -export function put_in_standard_position(curve: Curve): Curve { - const start_point = curve(0); - const curve_started_at_origin = translate( - -x_of(start_point), - -y_of(start_point), - 0 - )(curve); - const new_end_point = curve_started_at_origin(1); - const theta = Math.atan2(y_of(new_end_point), x_of(new_end_point)); - const curve_ended_at_x_axis = rotate_around_origin( - 0, - 0, - -theta - )(curve_started_at_origin); - const end_point_on_x_axis = x_of(curve_ended_at_x_axis(1)); - return scale_proportional(1 / end_point_on_x_axis)(curve_ended_at_x_axis); -} - -/** - * This function is a binary Curve operator: It takes two Curves as arguments - * and returns a new Curve. The two Curves are combined by using the full first - * Curve for the first portion of the result and by using the full second Curve - * for the second portion of the result. The second Curve is not changed, and - * therefore there might be a big jump in the middle of the result Curve. - * - * @param curve1 first Curve - * @param curve2 second Curve - * @returns result Curve - */ -export function connect_rigidly(curve1: Curve, curve2: Curve): Curve { - return (t) => (t < 1 / 2 ? curve1(2 * t) : curve2(2 * t - 1)); -} - -/** - * This function is a binary Curve operator: It takes two Curves as arguments - * and returns a new Curve. The two Curves are combined by using the full first - * Curve for the first portion of the result and by using the full second Curve - * for the second portion of the result. The second Curve is translated such - * that its point at fraction 0 is the same as the Point of the first Curve at - * fraction 1. - * - * @param curve1 first Curve - * @param curve2 second Curve - * @returns result Curve - */ -export function connect_ends(curve1: Curve, curve2: Curve): Curve { - const startPointOfCurve2 = curve2(0); - const endPointOfCurve1 = curve1(1); - return connect_rigidly( - curve1, - translate( - x_of(endPointOfCurve1) - x_of(startPointOfCurve2), - y_of(endPointOfCurve1) - y_of(startPointOfCurve2), - z_of(endPointOfCurve1) - z_of(startPointOfCurve2) - )(curve2) - ); -} - -/** - * This function is a curve: a function from a fraction t to a point. The points - * lie on the unit circle. They start at Point (1,0) when t is 0. When t is - * 0.25, they reach Point (0,1), when t is 0.5, they reach Point (-1, 0), etc. - * - * @param t fraction between 0 and 1 - * @returns Point on the circle at t - */ -export function unit_circle(t: number): Point { - return make_point(Math.cos(2 * Math.PI * t), Math.sin(2 * Math.PI * t)); -} - -/** - * This function is a curve: a function from a fraction t to a point. The - * x-coordinate at franction t is t, and the y-coordinate is 0. - * - * @param t fraction between 0 and 1 - * @returns Point on the line at t - */ -export function unit_line(t: number): Point { - return make_point(t, 0); -} - -/** - * This function is a Curve generator: it takes a number and returns a - * horizontal curve. The number is a y-coordinate, and the Curve generates only - * points with the given y-coordinate. - * - * @param t fraction between 0 and 1 - * @returns horizontal Curve - */ -export function unit_line_at(t: number): Curve { - return (a: number): Point => make_point(a, t); -} - -/** - * This function is a curve: a function from a fraction t to a point. The points - * lie on the right half of the unit circle. They start at Point (0,1) when t is - * 0. When t is 0.5, they reach Point (1,0), when t is 1, they reach Point - * (0, -1). - * - * @param t fraction between 0 and 1 - * @returns Point in the arc at t - */ -export function arc(t: number): Point { - return make_point(Math.sin(Math.PI * t), Math.cos(Math.PI * t)); -} - -/** - * Create a animation of curves using a curve generating function. - * @param duration The duration of the animation in seconds - * @param fps Framerate of the animation in frames per second - * @param drawer Draw function to the generated curves with - * @param func Curve generating function. Takes in a timestamp value and returns a curve - * @return Curve Animation - */ -export function animate_curve( - duration: number, - fps: number, - drawer: RenderFunction, - func: CurveAnimation -): AnimatedCurve { - if ((drawer as any).is3D) - throw new Error('Curve Animation cannot be used with 3D draw function!'); - - const anim = new AnimatedCurve(duration, fps, func, drawer, false); - drawnCurves.push(anim); - return anim; -} - -/** - * Create a animation of curves using a curve generating function. - * @param duration The duration of the animation in seconds - * @param fps Framerate of the animation in frames per second - * @param drawer Draw function to the generated curves with - * @param func Curve generating function. Takes in a timestamp value and returns a curve - * @return 3D Curve Animation - */ -export function animate_3D_curve( - duration: number, - fps: number, - drawer: RenderFunction, - func: CurveAnimation -): AnimatedCurve { - if (!(drawer as any).is3D) { - throw new Error('Curve 3D Animation cannot be used with 2D draw function!'); - } - - const anim = new AnimatedCurve(duration, fps, func, drawer, true); - drawnCurves.push(anim); - return anim; -} +/** + * drawing *curves*, i.e. collections of *points*, on a canvas in a tools tab + * + * A *point* is defined by its coordinates (x, y and z), and the color assigned to + * it (r, g, and b). A few constructors for points is given, for example + * `make_color_point`. Selectors allow access to the coordinates and color + * components, for example `x_of`. + * + * A *curve* is a + * unary function which takes a number argument within the unit interval `[0,1]` + * and returns a point. If `C` is a curve, then the starting point of the curve + * is always `C(0)`, and the ending point is always `C(1)`. + * + * A *curve transformation* is a function that takes a curve as argument and + * returns a curve. Examples of curve transformations are `scale` and `translate`. + * + * A *curve drawer* is function that takes a number argument and returns + * a function that takes a curve as argument and visualises it in the output screen is + * shown in the Source Academy in the tab with the "Curves Canvas" icon (image). + * The following [example](https://share.sourceacademy.org/unitcircle) uses + * the curve drawer `draw_connected_full_view` to display a curve called + * `unit_circle`. + * ``` + * import { make_point, draw_connected_full_view } from "curve"; + * function unit_circle(t) { + * return make_point(math_sin(2 * math_PI * t), + * math_cos(2 * math_PI * t)); + * } + * draw_connected_full_view(100)(unit_circle); + * ``` + * draws a full circle in the display tab. + * + * @module curve + * @author Lee Zheng Han + * @author Ng Yong Xiang + */ + +/* eslint-disable @typescript-eslint/naming-convention */ +import type { Curve, CurveDrawn } from './curves_webgl'; +import { generateCurve, Point } from './curves_webgl'; +import type { + CurveAnimation, + CurveSpace, + CurveTransformer, + DrawMode, + RenderFunction, + ScaleMode, +} from './types'; +import { + AnimatedCurve, +} from './types'; + +/** @hidden */ +export const drawnCurves: (CurveDrawn | AnimatedCurve)[] = []; + +function createDrawFunction( + scaleMode: ScaleMode, + drawMode: DrawMode, + space: CurveSpace, + isFullView: boolean, +): (numPoints: number) => RenderFunction { + return (numPoints: number) => { + const func = (curve) => { + const curveDrawn = generateCurve( + scaleMode, + drawMode, + numPoints, + curve, + space, + isFullView, + ); + + if ( + (curve as any).shouldAppend === undefined + || (curve as any).shouldAppend + ) { + drawnCurves.push(curveDrawn); + } + + return curveDrawn; + }; + // Because the draw functions are actually functions + // we need hacky workarounds like these to pass information around + func.is3D = space === '3D'; + return func; + }; +} + +// ============================================================================= +// Module's Exposed Functions +// +// This file only includes the implementation and documentation of exposed +// functions of the module. For private functions dealing with the browser's +// graphics library context, see './curves_webgl.ts'. +// ============================================================================= + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points and connecting each pair with a line. + * The parts between (0,0) and (1,1) of the resulting Drawing are shown in the window. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_connected(100)(t => make_point(t, t)); + * ``` + */ +export const draw_connected = createDrawFunction('none', 'lines', '2D', false); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points and connecting each pair with a line. The Drawing is + * translated and stretched/shrunk to show the full curve and maximize its width + * and height, with some padding. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_connected_full_view(100)(t => make_point(t, t)); + * ``` + */ +export const draw_connected_full_view = createDrawFunction( + 'stretch', + 'lines', + '2D', + true, +); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points and connecting each pair with a line. The Drawing + * is translated and scaled proportionally to show the full curve and maximize + * its size, with some padding. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_connected_full_view_proportional(100)(t => make_point(t, t)); + * ``` + */ +export const draw_connected_full_view_proportional = createDrawFunction( + 'fit', + 'lines', + '2D', + true, +); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points. The Drawing consists of isolated + * points, and does not connect them. The parts between (0,0) and (1,1) of the + * resulting Drawing are shown in the window. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1,there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_points(100)(t => make_point(t, t)); + * ``` + */ +export const draw_points = createDrawFunction('none', 'points', '2D', false); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points. The Drawing consists of isolated + * points, and does not connect them. The Drawing is translated and + * stretched/shrunk to show the full curve and maximize its width and height, + * with some padding. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_points_full_view(100)(t => make_point(t, t)); + * ``` + */ +export const draw_points_full_view = createDrawFunction( + 'stretch', + 'points', + '2D', + true, +); + +/** + * Returns a function that turns a given Curve into a Drawing, by sampling the + * Curve at `num` sample points. The Drawing consists of isolated + * points, and does not connect them. The Drawing is translated and scaled + * proportionally with its size maximized to fit entirely inside the window, + * with some padding. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_points_full_view_proportional(100)(t => make_point(t, t)); + * ``` + */ +export const draw_points_full_view_proportional = createDrawFunction( + 'fit', + 'points', + '2D', + true, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points and connecting each pair with + * a line. The parts between (0,0,0) and (1,1,1) of the resulting Drawing are + * shown within the unit cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_connected(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_connected = createDrawFunction( + 'none', + 'lines', + '3D', + false, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points and connecting each pair with + * a line. The Drawing is translated and stretched/shrunk to show the full + * curve and maximize its width and height within the cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_connected_full_view(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_connected_full_view = createDrawFunction( + 'stretch', + 'lines', + '3D', + false, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points and connecting each pair with + * a line. The Drawing is translated and scaled proportionally with its size + * maximized to fit entirely inside the cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_connected_full_view_proportional(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_connected_full_view_proportional = createDrawFunction( + 'fit', + 'lines', + '3D', + false, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points. The Drawing consists of + * isolated points, and does not connect them. The parts between (0,0,0) + * and (1,1,1) of the resulting Drawing are shown within the unit cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_points(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_points = createDrawFunction('none', 'points', '3D', false); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points. The Drawing consists of + * isolated points, and does not connect them. The Drawing is translated and + * stretched/shrunk to maximize its size to fit entirely inside the cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_points_full_view(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_points_full_view = createDrawFunction( + 'stretch', + 'points', + '3D', + false, +); + +/** + * Returns a function that turns a given 3D Curve into a Drawing, by sampling + * the 3D Curve at `num` sample points. The Drawing consists of + * isolated points, and does not connect them. The Drawing is translated and + * scaled proportionally with its size maximized to fit entirely inside the cube. + * + * @param num determines the number of points, lower than 65535, to be sampled. + * Including 0 and 1, there are `num + 1` evenly spaced sample points + * @return function of type Curve → Drawing + * @example + * ``` + * draw_3D_points_full_view_proportional(100)(t => make_3D_point(t, t, t)); + * ``` + */ +export const draw_3D_points_full_view_proportional = createDrawFunction( + 'fit', + 'points', + '3D', + false, +); + +/** + * Makes a Point with given x and y coordinates. + * + * @param x x-coordinate of new point + * @param y y-coordinate of new point + * @returns with x and y as coordinates + * @example + * ``` + * const point = make_point(0.5, 0.5); + * ``` + */ +export function make_point(x: number, y: number): Point { + return new Point(x, y, 0, [0, 0, 0, 1]); +} + +/** + * Makes a 3D Point with given x, y and z coordinates. + * + * @param x x-coordinate of new point + * @param y y-coordinate of new point + * @param z z-coordinate of new point + * @returns with x, y and z as coordinates + * @example + * ``` + * const point = make_3D_point(0.5, 0.5, 0.5); + * ``` + */ +export function make_3D_point(x: number, y: number, z: number): Point { + return new Point(x, y, z, [0, 0, 0, 1]); +} + +/** + * Makes a color Point with given x and y coordinates, and RGB values ranging + * from 0 to 255. Any input lower than 0 for RGB will be rounded up to 0, and + * any input higher than 255 will be rounded down to 255. + * + * @param x x-coordinate of new point + * @param y y-coordinate of new point + * @param r red component of new point + * @param g green component of new point + * @param b blue component of new point + * @returns with x and y as coordinates, and r, g and b as RGB values + * @example + * ``` + * const redPoint = make_color_point(0.5, 0.5, 255, 0, 0); + * ``` + */ +export function make_color_point( + x: number, + y: number, + r: number, + g: number, + b: number, +): Point { + return new Point(x, y, 0, [r / 255, g / 255, b / 255, 1]); +} + +/** + * Makes a 3D color Point with given x, y and z coordinates, and RGB values + * ranging from 0 to 255. Any input lower than 0 for RGB will be rounded up to + * 0, and any input higher than 255 will be rounded down to 255. + * + * @param x x-coordinate of new point + * @param y y-coordinate of new point + * @param z z-coordinate of new point + * @param r red component of new point + * @param g green component of new point + * @param b blue component of new point + * @returns with x, y and z as coordinates, and r, g and b as RGB values + * @example + * ``` + * const redPoint = make_color_point(0.5, 0.5, 0.5, 255, 0, 0); + * ``` + */ +export function make_3D_color_point( + x: number, + y: number, + z: number, + r: number, + g: number, + b: number, +): Point { + return new Point(x, y, z, [r / 255, g / 255, b / 255, 1]); +} + +/** + * Retrieves the x-coordinate of a given Point. + * + * @param p given point + * @returns x-coordinate of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * x_of(point); // Returns 1 + * ``` + */ +export function x_of(pt: Point): number { + return pt.x; +} + +/** + * Retrieves the y-coordinate of a given Point. + * + * @param p given point + * @returns y-coordinate of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * y_of(point); // Returns 2 + * ``` + */ +export function y_of(pt: Point): number { + return pt.y; +} + +/** + * Retrieves the z-coordinate of a given Point. + * + * @param p given point + * @returns z-coordinate of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * z_of(point); // Returns 3 + * ``` + */ +export function z_of(pt: Point): number { + return pt.z; +} + +/** + * Retrieves the red component of a given Point. + * + * @param p given point + * @returns Red component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * r_of(point); // Returns 50 + * ``` + */ +export function r_of(pt: Point): number { + return pt.color[0] * 255; +} + +/** + * Retrieves the green component of a given Point. + * + * @param p given point + * @returns Green component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * g_of(point); // Returns 100 + * ``` + */ +export function g_of(pt: Point): number { + return pt.color[1] * 255; +} + +/** + * Retrieves the blue component of a given Point. + * + * @param p given point + * @returns Blue component of the Point + * @example + * ``` + * const point = make_color_point(1, 2, 3, 50, 100, 150); + * b_of(point); // Returns 150 + * ``` + */ +export function b_of(pt: Point): number { + return pt.color[2] * 255; +} + +/** + * This function is a Curve transformation: a function from a Curve to a Curve. + * The points of the result Curve are the same points as the points of the + * original Curve, but in reverse: The result Curve applied to 0 is the original + * Curve applied to 1 and vice versa. + * + * @param original original Curve + * @returns result Curve + */ +export function invert(curve: Curve): Curve { + return (t: number) => curve(1 - t); +} + +/** + * This function returns a Curve transformation: It takes an x-value x0, a + * y-value y0 and a z-value z0, as arguments and + * returns a Curve transformation that takes a Curve as argument and returns a + * new Curve, by translating the original by x0 in x-direction, y0 in + * y-direction and z0 in z-direction. + * + * @param x0 x-value + * @param y0 y-value + * @param z0 z-value + * @returns Curve transformation + */ +export function translate( + x0: number, + y0: number, + z0: number, +): CurveTransformer { + return (curve: Curve) => { + const transformation = (cf: Curve) => (t: number) => { + const a = x0 === undefined ? 0 : x0; + const b = y0 === undefined ? 0 : y0; + const c = z0 === undefined ? 0 : z0; + const ct: Point = cf(t); + return make_3D_color_point( + a + x_of(ct), + b + y_of(ct), + c + z_of(ct), + r_of(ct), + g_of(ct), + b_of(ct), + ); + }; + return transformation(curve); + }; +} + +/** + * This function takes 3 angles, a, b and c in radians as parameter + * and returns a Curve transformation: a function that takes a Curve as argument + * and returns a new Curve, which is the original Curve rotated + * extrinsically with Euler angles (a, b, c) about x, y, + * and z axes. + * + * @param a given angle + * @param b given angle + * @param c given angle + * @returns function that takes a Curve and returns a Curve + */ +export function rotate_around_origin( + theta1: number, + theta2: number, + theta3: number, +): CurveTransformer { + if (theta3 === undefined && theta1 !== undefined && theta2 !== undefined) { + // 2 args + throw new Error('Expected 1 or 3 arguments, but received 2'); + } else if ( + theta1 !== undefined + && theta2 === undefined + && theta3 === undefined + ) { + // 1 args + const cth = Math.cos(theta1); + const sth = Math.sin(theta1); + return (curve: Curve) => { + const transformation = (c: Curve) => (t: number) => { + const ct = c(t); + const x = x_of(ct); + const y = y_of(ct); + const z = z_of(ct); + return make_3D_color_point( + cth * x - sth * y, + sth * x + cth * y, + z, + r_of(ct), + g_of(ct), + b_of(ct), + ); + }; + return transformation(curve); + }; + } else { + const cthx = Math.cos(theta1); + const sthx = Math.sin(theta1); + const cthy = Math.cos(theta2); + const sthy = Math.sin(theta2); + const cthz = Math.cos(theta3); + const sthz = Math.sin(theta3); + return (curve: Curve) => { + const transformation = (c: Curve) => (t: number) => { + const ct = c(t); + const coord = [x_of(ct), y_of(ct), z_of(ct)]; + const mat = [ + [ + cthz * cthy, + cthz * sthy * sthx - sthz * cthx, + cthz * sthy * cthx + sthz * sthx, + ], + [ + sthz * cthy, + sthz * sthy * sthx + cthz * cthx, + sthz * sthy * cthx - cthz * sthx, + ], + [-sthy, cthy * sthx, cthy * cthx], + ]; + let xf = 0; + let yf = 0; + let zf = 0; + for (let i = 0; i < 3; i += 1) { + xf += mat[0][i] * coord[i]; + yf += mat[1][i] * coord[i]; + zf += mat[2][i] * coord[i]; + } + return make_3D_color_point(xf, yf, zf, r_of(ct), g_of(ct), b_of(ct)); + }; + return transformation(curve); + }; + } +} + +/** + * This function takes scaling factors `a`, `b` and + * `c`, as arguments and returns a + * Curve transformation that scales a given Curve by `a` in + * x-direction, `b` in y-direction and `c` in z-direction. + * + * @param a scaling factor in x-direction + * @param b scaling factor in y-direction + * @param c scaling factor in z-direction + * @returns function that takes a Curve and returns a Curve + */ +export function scale(a: number, b: number, c: number): CurveTransformer { + return (curve) => { + const transformation = (cf: Curve) => (t: number) => { + const ct = cf(t); + const a1 = a === undefined ? 1 : a; + const b1 = b === undefined ? 1 : b; + const c1 = c === undefined ? 1 : c; + return make_3D_color_point( + a1 * x_of(ct), + b1 * y_of(ct), + c1 * z_of(ct), + r_of(ct), + g_of(ct), + b_of(ct), + ); + }; + return transformation(curve); + }; +} + +/** + * This function takes a scaling factor s argument and returns a Curve + * transformation that scales a given Curve by s in x, y and z direction. + * + * @param s scaling factor + * @returns function that takes a Curve and returns a Curve + */ +export function scale_proportional(s: number): CurveTransformer { + return scale(s, s, s); +} + +/** + * This function is a Curve transformation: It takes a Curve as argument and + * returns a new Curve, as follows. A Curve is in standard position if it + * starts at (0,0) ends at (1,0). This function puts the given Curve in + * standard position by rigidly translating it so its start Point is at the + * origin (0,0), then rotating it about the origin to put its endpoint on the + * x axis, then scaling it to put the endpoint at (1,0). Behavior is unspecified + * on closed Curves where start-point equal end-point. + * + * @param curve given Curve + * @returns result Curve + */ +export function put_in_standard_position(curve: Curve): Curve { + const start_point = curve(0); + const curve_started_at_origin = translate( + -x_of(start_point), + -y_of(start_point), + 0, + )(curve); + const new_end_point = curve_started_at_origin(1); + const theta = Math.atan2(y_of(new_end_point), x_of(new_end_point)); + const curve_ended_at_x_axis = rotate_around_origin( + 0, + 0, + -theta, + )(curve_started_at_origin); + const end_point_on_x_axis = x_of(curve_ended_at_x_axis(1)); + return scale_proportional(1 / end_point_on_x_axis)(curve_ended_at_x_axis); +} + +/** + * This function is a binary Curve operator: It takes two Curves as arguments + * and returns a new Curve. The two Curves are combined by using the full first + * Curve for the first portion of the result and by using the full second Curve + * for the second portion of the result. The second Curve is not changed, and + * therefore there might be a big jump in the middle of the result Curve. + * + * @param curve1 first Curve + * @param curve2 second Curve + * @returns result Curve + */ +export function connect_rigidly(curve1: Curve, curve2: Curve): Curve { + return (t) => (t < 1 / 2 ? curve1(2 * t) : curve2(2 * t - 1)); +} + +/** + * This function is a binary Curve operator: It takes two Curves as arguments + * and returns a new Curve. The two Curves are combined by using the full first + * Curve for the first portion of the result and by using the full second Curve + * for the second portion of the result. The second Curve is translated such + * that its point at fraction 0 is the same as the Point of the first Curve at + * fraction 1. + * + * @param curve1 first Curve + * @param curve2 second Curve + * @returns result Curve + */ +export function connect_ends(curve1: Curve, curve2: Curve): Curve { + const startPointOfCurve2 = curve2(0); + const endPointOfCurve1 = curve1(1); + return connect_rigidly( + curve1, + translate( + x_of(endPointOfCurve1) - x_of(startPointOfCurve2), + y_of(endPointOfCurve1) - y_of(startPointOfCurve2), + z_of(endPointOfCurve1) - z_of(startPointOfCurve2), + )(curve2), + ); +} + +/** + * This function is a curve: a function from a fraction t to a point. The points + * lie on the unit circle. They start at Point (1,0) when t is 0. When t is + * 0.25, they reach Point (0,1), when t is 0.5, they reach Point (-1, 0), etc. + * + * @param t fraction between 0 and 1 + * @returns Point on the circle at t + */ +export function unit_circle(t: number): Point { + return make_point(Math.cos(2 * Math.PI * t), Math.sin(2 * Math.PI * t)); +} + +/** + * This function is a curve: a function from a fraction t to a point. The + * x-coordinate at franction t is t, and the y-coordinate is 0. + * + * @param t fraction between 0 and 1 + * @returns Point on the line at t + */ +export function unit_line(t: number): Point { + return make_point(t, 0); +} + +/** + * This function is a Curve generator: it takes a number and returns a + * horizontal curve. The number is a y-coordinate, and the Curve generates only + * points with the given y-coordinate. + * + * @param t fraction between 0 and 1 + * @returns horizontal Curve + */ +export function unit_line_at(t: number): Curve { + return (a: number): Point => make_point(a, t); +} + +/** + * This function is a curve: a function from a fraction t to a point. The points + * lie on the right half of the unit circle. They start at Point (0,1) when t is + * 0. When t is 0.5, they reach Point (1,0), when t is 1, they reach Point + * (0, -1). + * + * @param t fraction between 0 and 1 + * @returns Point in the arc at t + */ +export function arc(t: number): Point { + return make_point(Math.sin(Math.PI * t), Math.cos(Math.PI * t)); +} + +/** + * Create a animation of curves using a curve generating function. + * @param duration The duration of the animation in seconds + * @param fps Framerate of the animation in frames per second + * @param drawer Draw function to the generated curves with + * @param func Curve generating function. Takes in a timestamp value and returns a curve + * @return Curve Animation + */ +export function animate_curve( + duration: number, + fps: number, + drawer: RenderFunction, + func: CurveAnimation, +): AnimatedCurve { + if ((drawer as any).is3D) { throw new Error('Curve Animation cannot be used with 3D draw function!'); } + + const anim = new AnimatedCurve(duration, fps, func, drawer, false); + drawnCurves.push(anim); + return anim; +} + +/** + * Create a animation of curves using a curve generating function. + * @param duration The duration of the animation in seconds + * @param fps Framerate of the animation in frames per second + * @param drawer Draw function to the generated curves with + * @param func Curve generating function. Takes in a timestamp value and returns a curve + * @return 3D Curve Animation + */ +export function animate_3D_curve( + duration: number, + fps: number, + drawer: RenderFunction, + func: CurveAnimation, +): AnimatedCurve { + if (!(drawer as any).is3D) { + throw new Error('Curve 3D Animation cannot be used with 2D draw function!'); + } + + const anim = new AnimatedCurve(duration, fps, func, drawer, true); + drawnCurves.push(anim); + return anim; +} diff --git a/src/bundles/curve/index.ts b/src/bundles/curve/index.ts index bf3b7d78e..bb9438fc0 100644 --- a/src/bundles/curve/index.ts +++ b/src/bundles/curve/index.ts @@ -1,4 +1,4 @@ -import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; +import type { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; import { animate_3D_curve, animate_curve, @@ -6,7 +6,6 @@ import { b_of, connect_ends, connect_rigidly, - drawnCurves, draw_3D_connected, draw_3D_connected_full_view, draw_3D_connected_full_view_proportional, @@ -19,6 +18,7 @@ import { draw_points, draw_points_full_view, draw_points_full_view_proportional, + drawnCurves, g_of, invert, make_3D_color_point, @@ -26,8 +26,8 @@ import { make_color_point, make_point, put_in_standard_position, - rotate_around_origin, r_of, + rotate_around_origin, scale, scale_proportional, translate, @@ -38,7 +38,7 @@ import { y_of, z_of, } from './functions'; -import { CurveModuleState } from './types'; +import type { CurveModuleState } from './types'; /** * Bundle for Source Academy Curves module @@ -48,7 +48,7 @@ import { CurveModuleState } from './types'; export default function curves( moduleParams: ModuleParams, - moduleContexts: ModuleContexts + moduleContexts: ModuleContexts, ) { // Update the module's global context let moduleContext = moduleContexts.get('curve'); @@ -73,7 +73,7 @@ export default function curves( (moduleContext.state as CurveModuleState).drawnCurves = drawnCurves; } - return { + return new Proxy({ make_point, make_3D_point, make_color_point, @@ -110,5 +110,10 @@ export default function curves( rotate_around_origin, arc, invert, - }; + }, { + get(target, name) { + if (target[name]) return target[name]; + throw new Error(`Undefined symbol: ${name.toString()}`); + }, + }); } diff --git a/src/bundles/curve/types.ts b/src/bundles/curve/types.ts index 4170af3b9..4a86dfb38 100644 --- a/src/bundles/curve/types.ts +++ b/src/bundles/curve/types.ts @@ -1,62 +1,63 @@ -/* eslint-disable max-classes-per-file */ - -import { ModuleState } from 'js-slang'; -import { glAnimation, AnimFrame } from '../../typings/anim_types'; -import { ReplResult } from '../../typings/type_helpers'; -import { Curve, CurveDrawn } from './curves_webgl'; - -/** A function that takes in CurveFunction and returns a tranformed CurveFunction. */ -export type CurveTransformer = (c: Curve) => Curve; - -export type DrawMode = 'lines' | 'points'; -export type ScaleMode = 'none' | 'stretch' | 'fit'; -export type CurveSpace = '2D' | '3D'; - -/** - * A function that takes in a timestamp and returns a Curve - */ -export type CurveAnimation = (t: number) => Curve; - -/** - * A function that specifies additional rendering information when taking in - * a CurveFunction and returns a ShapeDrawn based on its specifications. - */ -export type RenderFunction = (func: Curve) => CurveDrawn; - -export class AnimatedCurve extends glAnimation implements ReplResult { - constructor( - duration: number, - fps: number, - private readonly func: (timestamp: number) => Curve, - private readonly drawer: RenderFunction, - public readonly is3D: boolean - ) { - super(duration, fps); - this.angle = 0; - } - - public getFrame(timestamp: number): AnimFrame { - const curve = this.func(timestamp); - (curve as any).shouldAppend = false; - const curveDrawn = this.drawer(curve); - - return { - draw: (canvas: HTMLCanvasElement) => { - curveDrawn.init(canvas); - curveDrawn.redraw(this.angle); - }, - }; - } - - public angle: number; - - public toReplString = () => ''; -} - -export class CurveModuleState implements ModuleState { - constructor() { - this.drawnCurves = []; - } - - public drawnCurves: (CurveDrawn | AnimatedCurve)[]; -} +/* eslint-disable max-classes-per-file */ + +import type { ModuleState } from 'js-slang'; +import type { AnimFrame } from '../../typings/anim_types'; +import { glAnimation } from '../../typings/anim_types'; +import type { ReplResult } from '../../typings/type_helpers'; +import type { Curve, CurveDrawn } from './curves_webgl'; + +/** A function that takes in CurveFunction and returns a tranformed CurveFunction. */ +export type CurveTransformer = (c: Curve) => Curve; + +export type DrawMode = 'lines' | 'points'; +export type ScaleMode = 'none' | 'stretch' | 'fit'; +export type CurveSpace = '2D' | '3D'; + +/** + * A function that takes in a timestamp and returns a Curve + */ +export type CurveAnimation = (t: number) => Curve; + +/** + * A function that specifies additional rendering information when taking in + * a CurveFunction and returns a ShapeDrawn based on its specifications. + */ +export type RenderFunction = (func: Curve) => CurveDrawn; + +export class AnimatedCurve extends glAnimation implements ReplResult { + constructor( + duration: number, + fps: number, + private readonly func: (timestamp: number) => Curve, + private readonly drawer: RenderFunction, + public readonly is3D: boolean, + ) { + super(duration, fps); + this.angle = 0; + } + + public getFrame(timestamp: number): AnimFrame { + const curve = this.func(timestamp); + (curve as any).shouldAppend = false; + const curveDrawn = this.drawer(curve); + + return { + draw: (canvas: HTMLCanvasElement) => { + curveDrawn.init(canvas); + curveDrawn.redraw(this.angle); + }, + }; + } + + public angle: number; + + public toReplString = () => ''; +} + +export class CurveModuleState implements ModuleState { + constructor() { + this.drawnCurves = []; + } + + public drawnCurves: (CurveDrawn | AnimatedCurve)[]; +} diff --git a/src/bundles/game/functions.ts b/src/bundles/game/functions.ts index 73935c93c..7fc46e638 100644 --- a/src/bundles/game/functions.ts +++ b/src/bundles/game/functions.ts @@ -1,1494 +1,1526 @@ -/** - * Game library that translates Phaser 3 API into Source. - * - * More in-depth explanation of the Phaser 3 API can be found at - * Phaser 3 documentation itself. - * - * For Phaser 3 API Documentation, check: - * https://photonstorm.github.io/phaser3-docs/ - * - * @module game - * @author Anthony Halim - * @author Chi Xu - * @author Chong Sia Tiffany - * @author Gokul Rajiv - */ - -/* eslint-disable @typescript-eslint/no-unused-vars, consistent-return, @typescript-eslint/no-shadow, @typescript-eslint/default-param-last */ -import { - GameModuleParams, - GameObject, - List, - ObjectConfig, - RawContainer, - RawGameElement, - RawGameObject, - RawInputObject, -} from './types'; - -/** @hidden */ -export default function gameFuncs(moduleParams: GameModuleParams) { - const { - scene, - preloadImageMap, - preloadSoundMap, - preloadSpritesheetMap, - remotePath, - screenSize, - createAward, - } = moduleParams.game || {}; - - // Listener ObjectTypes - enum ListenerTypes { - InputPlugin = 'input_plugin', - KeyboardKeyType = 'keyboard_key', - } - - const ListnerTypes = Object.values(ListenerTypes); - - // Object ObjectTypes - enum ObjectTypes { - ImageType = 'image', - TextType = 'text', - RectType = 'rect', - EllipseType = 'ellipse', - ContainerType = 'container', - AwardType = 'award', - } - - const ObjTypes = Object.values(ObjectTypes); - - const nullFn = () => {}; - - // ============================================================================= - // Module's Private Functions - // ============================================================================= - - /** @hidden */ - function get_obj( - obj: GameObject - ): RawGameObject | RawInputObject | RawContainer { - return obj.object!; - } - - /** @hidden */ - function get_game_obj(obj: GameObject): RawGameObject | RawContainer { - return obj.object as RawGameObject | RawContainer; - } - - /** @hidden */ - function get_input_obj(obj: GameObject): RawInputObject { - return obj.object as RawInputObject; - } - - /** @hidden */ - function get_container(obj: GameObject): RawContainer { - return obj.object as RawContainer; - } - - /** - * Checks whether the given game object is of the enquired type. - * If the given obj is undefined, will also return false. - * - * @param obj the game object - * @param type enquired type - * @returns if game object is of enquired type - * @hidden - */ - function is_type(obj: GameObject, type: string): boolean { - return obj !== undefined && obj.type === type && obj.object !== undefined; - } - - /** - * Checks whether the given game object is any of the enquired ObjectTypes - * - * @param obj the game object - * @param ObjectTypes enquired ObjectTypes - * @returns if game object is of any of the enquired ObjectTypes - * @hidden - */ - function is_any_type(obj: GameObject, types: string[]): boolean { - // eslint-disable-next-line no-plusplus - for (let i = 0; i < types.length; ++i) { - if (is_type(obj, types[i])) return true; - } - return false; - } - - /** - * Set a game object to the given type. - * Mutates the object. - * - * @param object the game object - * @param type type to set - * @returns typed game object - * @hidden - */ - function set_type( - object: RawGameObject | RawInputObject | RawContainer, - type: string - ): GameObject { - return { type, object }; - } - - /** - * Throw a console error, including the function caller name. - * - * @param {string} message error message - * @hidden - */ - function throw_error(message: string) { - // eslint-disable-next-line no-console, no-caller, @typescript-eslint/no-throw-literal, no-restricted-properties - throw console.error(`${arguments.callee.caller.name}: ${message}`); - } - - // List processing - // Original Author: Martin Henz - - /** - * array test works differently for Rhino and - * the Firefox environment (especially Web Console) - */ - function array_test(x: any): boolean { - if (Array.isArray === undefined) { - return x instanceof Array; - } - return Array.isArray(x); - } - - /** - * pair constructs a pair using a two-element array - * LOW-LEVEL FUNCTION, NOT SOURCE - */ - function pair(x: any, xs: any): [any, any] { - return [x, xs]; - } - - /** - * is_pair returns true iff arg is a two-element array - * LOW-LEVEL FUNCTION, NOT SOURCE - */ - function is_pair(x: any): boolean { - return array_test(x) && x.length === 2; - } - - /** - * head returns the first component of the given pair, - * throws an exception if the argument is not a pair - * LOW-LEVEL FUNCTION, NOT SOURCE - */ - function head(xs: List): any { - if (is_pair(xs)) { - return xs![0]; - } - throw new Error( - `head(xs) expects a pair as argument xs, but encountered ${xs}` - ); - } - - /** - * tail returns the second component of the given pair - * throws an exception if the argument is not a pair - * LOW-LEVEL FUNCTION, NOT SOURCE - */ - function tail(xs: List) { - if (is_pair(xs)) { - return xs![1]; - } - throw new Error( - `tail(xs) expects a pair as argument xs, but encountered ${xs}` - ); - } - - /** - * is_null returns true if arg is exactly null - * LOW-LEVEL FUNCTION, NOT SOURCE - */ - function is_null(xs: any) { - return xs === null; - } - - /** - * map applies first arg f to the elements of the second argument, - * assumed to be a list. - * f is applied element-by-element: - * map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] - * map throws an exception if the second argument is not a list, - * and if the second argument is a non-empty list and the first - * argument is not a function. - */ - function map(f: (x: any) => any, xs: List) { - return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); - } - - // ============================================================================= - // Module's Exposed Functions - // ============================================================================= - - // HELPER - - function prepend_remote_url(asset_key: string): string { - return remotePath(asset_key); - } - - function create_config(lst: List): ObjectConfig { - const config = {}; - map((xs: [any, any]) => { - if (!is_pair(xs)) { - throw_error(`xs is not pair!`); - } - config[head(xs)] = tail(xs); - }, lst); - return config; - } - - function create_text_config( - font_family: string = 'Courier', - font_size: string = '16px', - color: string = '#fff', - stroke: string = '#fff', - stroke_thickness: number = 0, - align: string = 'left' - ): ObjectConfig { - return { - fontFamily: font_family, - fontSize: font_size, - color, - stroke, - strokeThickness: stroke_thickness, - align, - }; - } - - function create_interactive_config( - draggable: boolean = false, - use_hand_cursor: boolean = false, - pixel_perfect: boolean = false, - alpha_tolerance: number = 1 - ): ObjectConfig { - return { - draggable, - useHandCursor: use_hand_cursor, - pixelPerfect: pixel_perfect, - alphaTolerance: alpha_tolerance, - }; - } - - function create_sound_config( - mute: boolean = false, - volume: number = 1, - rate: number = 1, - detune: number = 0, - seek: number = 0, - loop: boolean = false, - delay: number = 0 - ): ObjectConfig { - return { - mute, - volume, - rate, - detune, - seek, - loop, - delay, - }; - } - - function create_tween_config( - target_prop: string = 'x', - target_value: string | number = 0, - delay: number = 0, - duration: number = 1000, - ease: Function | string = 'Power0', - on_complete: Function = nullFn, - yoyo: boolean = false, - loop: number = 0, - loop_delay: number = 0, - on_loop: Function = nullFn - ): ObjectConfig { - return { - [target_prop]: target_value, - delay, - duration, - ease, - onComplete: on_complete, - yoyo, - loop, - loopDelay: loop_delay, - onLoop: on_loop, - }; - } - - function create_anim_config( - anims_key: string, - anim_frames: ObjectConfig[], - frame_rate: number = 24, - duration: any = null, - repeat: number = -1, - yoyo: boolean = false, - show_on_start: boolean = true, - hide_on_complete: boolean = false - ): ObjectConfig { - return { - key: anims_key, - frames: anim_frames, - frameRate: frame_rate, - duration, - repeat, - yoyo, - showOnStart: show_on_start, - hideOnComplete: hide_on_complete, - }; - } - - function create_anim_frame_config( - key: string, - duration: number = 0, - visible: boolean = true - ): ObjectConfig { - return { - key, - duration, - visible, - }; - } - - function create_anim_spritesheet_frame_configs( - key: string - ): ObjectConfig[] | undefined { - if (preloadSpritesheetMap.get(key)) { - const configArr = scene.anims.generateFrameNumbers(key, {}); - return configArr; - } - throw_error(`${key} is not associated with any spritesheet`); - } - - function create_spritesheet_config( - frame_width: number, - frame_height: number, - start_frame: number = 0, - margin: number = 0, - spacing: number = 0 - ): ObjectConfig { - return { - frameWidth: frame_width, - frameHeight: frame_height, - startFrame: start_frame, - margin, - spacing, - }; - } - - // SCREEN - - function get_screen_width(): number { - return screenSize.x; - } - - function get_screen_height(): number { - return screenSize.y; - } - - function get_screen_display_width(): number { - return scene.scale.displaySize.width; - } - - function get_screen_display_height(): number { - return scene.scale.displaySize.height; - } - - // LOAD - - function load_image(key: string, url: string) { - preloadImageMap.set(key, url); - } - - function load_sound(key: string, url: string) { - preloadSoundMap.set(key, url); - } - - function load_spritesheet( - key: string, - url: string, - spritesheet_config: ObjectConfig - ) { - preloadSpritesheetMap.set(key, [url, spritesheet_config]); - } - - // ADD - - function add(obj: GameObject): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - scene.add.existing(get_game_obj(obj)); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - // SOUND - - function play_sound(key: string, config: ObjectConfig = {}): void { - if (preloadSoundMap.get(key)) { - scene.sound.play(key, config); - } else { - throw_error(`${key} is not associated with any sound`); - } - } - - // ANIMS - - function create_anim(anim_config: ObjectConfig): boolean { - const anims = scene.anims.create(anim_config); - return typeof anims !== 'boolean'; - } - - function play_anim_on_image( - image: GameObject, - anims_key: string - ): GameObject | undefined { - if (is_type(image, ObjectTypes.ImageType)) { - (get_obj(image) as Phaser.GameObjects.Sprite).play(anims_key); - return image; - } - throw_error(`${image} is not of type ${ObjectTypes.ImageType}`); - } - - // IMAGE - - function create_image( - x: number, - y: number, - asset_key: string - ): GameObject | undefined { - if ( - preloadImageMap.get(asset_key) || - preloadSpritesheetMap.get(asset_key) - ) { - const image = new Phaser.GameObjects.Sprite(scene, x, y, asset_key); - return set_type(image, ObjectTypes.ImageType); - } - throw_error(`${asset_key} is not associated with any image`); - } - - // AWARD - - function create_award(x: number, y: number, award_key: string): GameObject { - return set_type(createAward(x, y, award_key), ObjectTypes.AwardType); - } - - // TEXT - - function create_text( - x: number, - y: number, - text: string, - config: ObjectConfig = {} - ): GameObject { - const txt = new Phaser.GameObjects.Text(scene, x, y, text, config); - return set_type(txt, ObjectTypes.TextType); - } - - // RECTANGLE - - function create_rect( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1 - ): GameObject { - const rect = new Phaser.GameObjects.Rectangle( - scene, - x, - y, - width, - height, - fill, - alpha - ); - return set_type(rect, ObjectTypes.RectType); - } - - // ELLIPSE - - function create_ellipse( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1 - ): GameObject { - const ellipse = new Phaser.GameObjects.Ellipse( - scene, - x, - y, - width, - height, - fill, - alpha - ); - return set_type(ellipse, ObjectTypes.EllipseType); - } - - // CONTAINER - - function create_container(x: number, y: number): GameObject { - const cont = new Phaser.GameObjects.Container(scene, x, y); - return set_type(cont, ObjectTypes.ContainerType); - } - - function add_to_container( - container: GameObject, - obj: GameObject - ): GameObject | undefined { - if ( - is_type(container, ObjectTypes.ContainerType) && - is_any_type(obj, ObjTypes) - ) { - get_container(container).add(get_game_obj(obj)); - return container; - } - throw_error( - `${obj} is not of type ${ObjTypes} or ${container} is not of type ${ObjectTypes.ContainerType}` - ); - } - - // OBJECT - - function destroy_obj(obj: GameObject) { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).destroy(); - } else { - throw_error(`${obj} is not of type ${ObjTypes}`); - } - } - - function set_display_size( - obj: GameObject, - x: number, - y: number - ): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setDisplaySize(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_alpha(obj: GameObject, alpha: number): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setAlpha(alpha); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_interactive( - obj: GameObject, - config: ObjectConfig = {} - ): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setInteractive(config); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_origin( - obj: GameObject, - x: number, - y: number - ): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - (get_game_obj(obj) as RawGameObject).setOrigin(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_position( - obj: GameObject, - x: number, - y: number - ): GameObject | undefined { - if (obj && is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setPosition(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_scale( - obj: GameObject, - x: number, - y: number - ): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setScale(x, y); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_rotation(obj: GameObject, rad: number): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - get_game_obj(obj).setRotation(rad); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function set_flip( - obj: GameObject, - x: boolean, - y: boolean - ): GameObject | undefined { - const GameElementType = [ObjectTypes.ImageType, ObjectTypes.TextType]; - if (is_any_type(obj, GameElementType)) { - (get_obj(obj) as RawGameElement).setFlip(x, y); - return obj; - } - throw_error(`${obj} is not of type ${GameElementType}`); - } - - async function add_tween( - obj: GameObject, - config: ObjectConfig = {} - ): Promise { - if (is_any_type(obj, ObjTypes)) { - scene.tweens.add({ - targets: get_game_obj(obj), - ...config, - }); - return obj; - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - // LISTENER - - function add_listener( - obj: GameObject, - event: string, - callback: Function - ): GameObject | undefined { - if (is_any_type(obj, ObjTypes)) { - const listener = get_game_obj(obj).addListener(event, callback); - return set_type(listener, ListenerTypes.InputPlugin); - } - throw_error(`${obj} is not of type ${ObjTypes}`); - } - - function add_keyboard_listener( - key: string | number, - event: string, - callback: Function - ): GameObject { - const keyObj = scene.input.keyboard.addKey(key); - const keyboardListener = keyObj.addListener(event, callback); - return set_type(keyboardListener, ListenerTypes.KeyboardKeyType); - } - - function remove_listener(listener: GameObject): boolean { - if (is_any_type(listener, ListnerTypes)) { - get_input_obj(listener).removeAllListeners(); - return true; - } - return false; - } - - const functions = { - add, - add_listener, - add_keyboard_listener, - add_to_container, - add_tween, - create_anim, - create_anim_config, - create_anim_frame_config, - create_anim_spritesheet_frame_configs, - create_award, - create_config, - create_container, - create_ellipse, - create_image, - create_interactive_config, - create_rect, - create_text, - create_text_config, - create_tween_config, - create_sound_config, - create_spritesheet_config, - destroy_obj, - get_screen_width, - get_screen_height, - get_screen_display_width, - get_screen_display_height, - load_image, - load_sound, - load_spritesheet, - play_anim_on_image, - play_sound, - prepend_remote_url, - remove_listener, - set_alpha, - set_display_size, - set_flip, - set_interactive, - set_origin, - set_position, - set_rotation, - set_scale, - }; - - const finalFunctions = {}; - - // eslint-disable-next-line array-callback-return - Object.entries(functions).map(([key, fn]) => { - finalFunctions[key] = !scene ? nullFn : fn; - }); - - return finalFunctions; -} - -// ============================================================================= -// Dummy functions for TypeDoc -// -// Refer to functions of matching signature in `gameFuncs` -// for implementation details -// ============================================================================= - -/** - * Prepend the given asset key with the remote path (S3 path). - * - * @param asset_key - * @returns prepended path - */ -export function prepend_remote_url(asset_key: string): string { - return ''; -} - -/** - * Transforms the given list into an object config. The list follows - * the format of list([key1, value1], [key2, value2]). - * - * e.g list(["alpha", 0], ["duration", 1000]) - * - * @param lst the list to be turned into object config. - * @returns object config - */ -export function create_config(lst: List): ObjectConfig { - return {}; -} - -/** - * Create text config object, can be used to stylise text object. - * - * font_family: for available font_family, see: - * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Valid_family_names - * - * align: must be either 'left', 'right', 'center', or 'justify' - * - * For more details about text config, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.GameObjects.Text.html#.TextStyle - * - * @param font_family font to be used - * @param font_size size of font, must be appended with 'px' e.g. '16px' - * @param color colour of font, in hex e.g. '#fff' - * @param stroke colour of stroke, in hex e.g. '#fff' - * @param stroke_thickness thickness of stroke - * @param align text alignment - * @returns text config - */ -export function create_text_config( - font_family: string = 'Courier', - font_size: string = '16px', - color: string = '#fff', - stroke: string = '#fff', - stroke_thickness: number = 0, - align: string = 'left' -): ObjectConfig { - return {}; -} - -/** - * Create interactive config object, can be used to configure interactive settings. - * - * For more details about interactive config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Input.html#.InputConfiguration - * - * @param draggable object will be set draggable - * @param use_hand_cursor if true, pointer will be set to 'pointer' when a pointer is over it - * @param pixel_perfect pixel perfect function will be set for the hit area. Only works for texture based object - * @param alpha_tolerance if pixel_perfect is set, this is the alpha tolerance threshold value used in the callback - * @returns interactive config - */ -export function create_interactive_config( - draggable: boolean = false, - use_hand_cursor: boolean = false, - pixel_perfect: boolean = false, - alpha_tolerance: number = 1 -): ObjectConfig { - return {}; -} - -/** - * Create sound config object, can be used to configure sound settings. - * - * For more details about sound config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Sound.html#.SoundConfig - * - * @param mute whether the sound should be muted or not - * @param volume value between 0(silence) and 1(full volume) - * @param rate the speed at which the sound is played - * @param detune detuning of the sound, in cents - * @param seek position of playback for the sound, in seconds - * @param loop whether or not the sound should loop - * @param delay time, in seconds, that elapse before the sound actually starts - * @returns sound config - */ -export function create_sound_config( - mute: boolean = false, - volume: number = 1, - rate: number = 1, - detune: number = 0, - seek: number = 0, - loop: boolean = false, - delay: number = 0 -): ObjectConfig { - return {}; -} - -/** - * Create tween config object, can be used to configure tween settings. - * - * For more details about tween config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Tweens.html#.TweenBuilderConfig - * - * @param target_prop target to tween, e.g. x, y, alpha - * @param target_value the property value to tween to - * @param delay time in ms/frames before tween will start - * @param duration duration of tween in ms/frames, exclude yoyos or repeats - * @param ease ease function to use, e.g. 'Power0', 'Power1', 'Power2' - * @param on_complete function to execute when tween completes - * @param yoyo if set to true, once tween complete, reverses the values incrementally to get back to the starting tween values - * @param loop number of times the tween should loop, or -1 to loop indefinitely - * @param loop_delay The time the tween will pause before starting either a yoyo or returning to the start for a repeat - * @param on_loop function to execute each time the tween loops - * @returns tween config - */ -export function create_tween_config( - target_prop: string = 'x', - target_value: string | number = 0, - delay: number = 0, - duration: number = 1000, - ease: Function | string = 'Power0', - on_complete: Function, - yoyo: boolean = false, - loop: number = 0, - loop_delay: number = 0, - on_loop: Function -): ObjectConfig { - return {}; -} - -/** - * Create anims config, can be used to configure anims - * - * For more details about the config object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Animations.html#.Animation - * - * @param anims_key key that the animation will be associated with - * @param anim_frames data used to generate the frames for animation - * @param frame_rate frame rate of playback in frames per second - * @param duration how long the animation should play in seconds. - * If null, will be derived from frame_rate - * @param repeat number of times to repeat the animation, -1 for infinity - * @param yoyo should the animation yoyo (reverse back down to the start) - * @param show_on_start should the sprite be visible when the anims start? - * @param hide_on_complete should the sprite be not visible when the anims finish? - * @returns animation config - */ -export function create_anim_config( - anims_key: string, - anim_frames: ObjectConfig[], - frame_rate: number = 24, - duration: any = null, - repeat: number = -1, - yoyo: boolean = false, - show_on_start: boolean = true, - hide_on_complete: boolean = false -): ObjectConfig { - return {}; -} - -/** - * Create animation frame config, can be used to configure a specific frame - * within an animation. - * - * The key should refer to an image that is already loaded. - * To make frame_config from spritesheet based on its frames, - * use create_anim_spritesheet_frame_configs instead. - * - * @param key key that is associated with the sprite at this frame - * @param duration duration, in ms, of this frame of the animation - * @param visible should the parent object be visible during this frame? - * @returns animation frame config - */ -export function create_anim_frame_config( - key: string, - duration: number = 0, - visible: boolean = true -): ObjectConfig { - return {}; -} - -/** - * Create list of animation frame config, can be used directly as part of - * anim_config's `frames` parameter. - * - * This function will generate list of frame configs based on the - * spritesheet_config attached to the associated spritesheet. - * This function requires that the given key is a spritesheet key - * i.e. a key associated with loaded spritesheet, loaded in using - * load_spritesheet function. - * - * Will return empty frame configs if key is not associated with - * a spritesheet. - * - * @param key key associated with spritesheet - * @returns animation frame configs - */ -export function create_anim_spritesheet_frame_configs( - key: string -): ObjectConfig[] | undefined { - return undefined; -} - -/** - * Create spritesheet config, can be used to configure the frames within the - * spritesheet. Can be used as config at load_spritesheet. - * - * @param frame_width width of frame in pixels - * @param frame_height height of frame in pixels - * @param start_frame first frame to start parsing from - * @param margin margin in the image; this is the space around the edge of the frames - * @param spacing the spacing between each frame in the image - * @returns spritesheet config - */ -export function create_spritesheet_config( - frame_width: number, - frame_height: number, - start_frame: number = 0, - margin: number = 0, - spacing: number = 0 -): ObjectConfig { - return {}; -} - -// SCREEN - -/** - * Get in-game screen width. - * - * @return screen width - */ -export function get_screen_width(): number { - return -1; -} - -/** - * Get in-game screen height. - * - * @return screen height - */ -export function get_screen_height(): number { - return -1; -} - -/** - * Get game screen display width (accounting window size). - * - * @return screen display width - */ -export function get_screen_display_width(): number { - return -1; -} - -/** - * Get game screen display height (accounting window size). - * - * @return screen display height - */ -export function get_screen_display_height(): number { - return -1; -} - -// LOAD - -/** - * Load the image asset into the scene for use. All images - * must be loaded before used in create_image. - * - * @param key key to be associated with the image - * @param url path to the image - */ -export function load_image(key: string, url: string) {} - -/** - * Load the sound asset into the scene for use. All sound - * must be loaded before used in play_sound. - * - * @param key key to be associated with the sound - * @param url path to the sound - */ -export function load_sound(key: string, url: string) {} - -/** - * Load the spritesheet into the scene for use. All spritesheet must - * be loaded before used in create_image. - * - * @param key key associated with the spritesheet - * @param url path to the sound - * @param spritesheet_config config to determines frames within the spritesheet - */ -export function load_spritesheet( - key: string, - url: string, - spritesheet_config: ObjectConfig -) { - return {}; -} - -// ADD - -/** - * Add the object to the scene. Only objects added to the scene - * will appear. - * - * @param obj game object to be added - */ -export function add(obj: GameObject): GameObject | undefined { - return undefined; -} - -// SOUND - -/** - * Play the sound associated with the key. - * Throws error if key is non-existent. - * - * @param key key to the sound to be played - * @param config sound config to be used - */ -export function play_sound(key: string, config: ObjectConfig = {}): void {} - -// ANIMS - -/** - * Create a new animation and add it to the available animations. - * Animations are global i.e. once created, it can be used anytime, anywhere. - * - * NOTE: Anims DO NOT need to be added into the scene to be used. - * It is automatically added to the scene when it is created. - * - * Will return true if the animation key is valid - * (key is specified within the anim_config); false if the key - * is already in use. - * - * @param anim_config - * @returns true if animation is successfully created, false otherwise - */ -export function create_anim(anim_config: ObjectConfig): boolean { - return false; -} - -/** - * Start playing the given animation on image game object. - * - * @param image image game object - * @param anims_key key associated with an animation - */ -export function play_anim_on_image( - image: GameObject, - anims_key: string -): GameObject | undefined { - return undefined; -} - -// IMAGE - -/** - * Create an image using the key associated with a loaded image. - * If key is not associated with any loaded image, throws error. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x position of the image. 0 is at the left side - * @param y y position of the image. 0 is at the top side - * @param asset_key key to loaded image - * @returns image game object - */ -export function create_image( - x: number, - y: number, - asset_key: string -): GameObject | undefined { - return undefined; -} - -// AWARD - -/** - * Create an award using the key associated with the award. - * The award key can be obtained from the Awards Hall or - * Awards menu, after attaining the award. - * - * Valid award will have an on-hover VERIFIED tag to distinguish - * it from images created by create_image. - * - * If student does not possess the award, this function will - * return a untagged, default image. - * - * @param x x position of the image. 0 is at the left side - * @param y y position of the image. 0 is at the top side - * @param award_key key for award - * @returns award game object - */ -export function create_award( - x: number, - y: number, - award_key: string -): GameObject { - return { type: 'null', object: undefined }; -} - -// TEXT - -/** - * Create a text object. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x position of the text - * @param y y position of the text - * @param text text to be shown - * @param config text configuration to be used - * @returns text game object - */ -export function create_text( - x: number, - y: number, - text: string, - config: ObjectConfig = {} -): GameObject { - return { type: 'null', object: undefined }; -} - -// RECTANGLE - -/** - * Create a rectangle object. - * - * 0, 0 is located at the top, left hand side. - * - * @param x x coordinate of the top, left corner posiiton - * @param y y coordinate of the top, left corner position - * @param width width of rectangle - * @param height height of rectangle - * @param fill colour fill, in hext e.g 0xffffff - * @param alpha value between 0 and 1 to denote alpha - * @returns rectangle object - */ -export function create_rect( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1 -): GameObject { - return { type: 'null', object: undefined }; -} - -// ELLIPSE - -/** - * Create an ellipse object. - * - * @param x x coordinate of the centre of ellipse - * @param y y coordinate of the centre of ellipse - * @param width width of ellipse - * @param height height of ellipse - * @param fill colour fill, in hext e.g 0xffffff - * @param alpha value between 0 and 1 to denote alpha - * @returns ellipse object - */ -export function create_ellipse( - x: number, - y: number, - width: number, - height: number, - fill: number = 0, - alpha: number = 1 -): GameObject { - return { type: 'null', object: undefined }; -} - -// CONTAINER - -/** - * Create a container object. Container is able to contain any other game object, - * and the positions of contained game object will be relative to the container. - * - * Rendering the container as visible or invisible will also affect the contained - * game object. - * - * Container can also contain another container. - * - * 0, 0 is located at the top, left hand side. - * - * For more details about container object, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Container.html - * - * @param x x position of the container - * @param y y position of the container - * @returns container object - */ -export function create_container(x: number, y: number): GameObject { - return { type: 'container', object: undefined }; -} - -/** - * Add the given game object to the container. - * Mutates the container. - * - * @param container container object - * @param obj game object to add to the container - * @returns container object - */ -export function add_to_container( - container: GameObject, - obj: GameObject -): GameObject | undefined { - return undefined; -} - -// OBJECT - -/** - * Destroy the given game object. Destroyed game object - * is removed from the scene, and all of its listeners - * is also removed. - * - * @param obj game object itself - */ -export function destroy_obj(obj: GameObject) {} - -/** - * Set the display size of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new display width size - * @param y new display height size - * @returns game object itself - */ -export function set_display_size( - obj: GameObject, - x: number, - y: number -): GameObject | undefined { - return undefined; -} - -/** - * Set the alpha of the object. - * Mutate the object. - * - * @param obj object to be set - * @param alpha new alpha - * @returns game object itself - */ -export function set_alpha( - obj: GameObject, - alpha: number -): GameObject | undefined { - return undefined; -} - -/** - * Set the interactivity of the object. - * Mutate the object. - * - * Rectangle and Ellipse are not able to receive configs, only boolean - * i.e. set_interactive(rect, true); set_interactive(ellipse, false) - * - * @param obj object to be set - * @param config interactive config to be used - * @returns game object itself - */ -export function set_interactive( - obj: GameObject, - config: ObjectConfig = {} -): GameObject | undefined { - return undefined; -} - -/** - * Set the origin in which all position related will be relative to. - * In other words, the anchor of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new anchor x coordinate, between value 0 to 1. - * @param y new anchor y coordinate, between value 0 to 1. - * @returns game object itself - */ -export function set_origin( - obj: GameObject, - x: number, - y: number -): GameObject | undefined { - return undefined; -} - -/** - * Set the position of the game object - * Mutate the object - * - * @param obj object to be set - * @param x new x position - * @param y new y position - * @returns game object itself - */ -export function set_position( - obj: GameObject, - x: number, - y: number -): GameObject | undefined { - return undefined; -} - -/** - * Set the scale of the object. - * Mutate the object. - * - * @param obj object to be set - * @param x new x scale - * @param y new y scale - * @returns game object itself - */ -export function set_scale( - obj: GameObject, - x: number, - y: number -): GameObject | undefined { - return undefined; -} - -/** - * Set the rotation of the object. - * Mutate the object. - * - * @param obj object to be set - * @param rad the rotation, in radians - * @returns game object itself - */ -export function set_rotation( - obj: GameObject, - rad: number -): GameObject | undefined { - return undefined; -} - -/** - * Sets the horizontal and flipped state of the object. - * Mutate the object. - * - * @param obj game object itself - * @param x to flip in the horizontal state - * @param y to flip in the vertical state - * @returns game object itself - */ -export function set_flip( - obj: GameObject, - x: boolean, - y: boolean -): GameObject | undefined { - return undefined; -} - -/** - * Creates a tween to the object and plays it. - * Mutate the object. - * - * @param obj object to be added to - * @param config tween config - * @returns game object itself - */ -export async function add_tween( - obj: GameObject, - config: ObjectConfig = {} -): Promise { - return undefined; -} - -// LISTENER - -/** - * Attach a listener to the object. The callback will be executed - * when the event is emitted. - * Mutate the object. - * - * For all available events, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html - * - * @param obj object to be added to - * @param event the event name - * @param callback listener function, executed on event - * @returns listener game object - */ -export function add_listener( - obj: GameObject, - event: string, - callback: Function -): GameObject | undefined { - return undefined; -} - -/** - * Attach a listener to the object. The callback will be executed - * when the event is emitted. - * Mutate the object. - * - * For all available events, see: - * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html - * - * For list of keycodes, see: - * https://github.com/photonstorm/phaser/blob/v3.22.0/src/input/keyboard/keys/KeyCodes.js - * - * @param key keyboard key to trigger listener - * @param event the event name - * @param callback listener function, executed on event - * @returns listener game object - */ -export function add_keyboard_listener( - key: string | number, - event: string, - callback: Function -): GameObject { - return { type: 'null', object: undefined }; -} - -/** - * Deactivate and remove listener. - * - * @param listener - * @returns if successful - */ -export function remove_listener(listener: GameObject): boolean { - return false; -} +/** + * Game library that translates Phaser 3 API into Source. + * + * More in-depth explanation of the Phaser 3 API can be found at + * Phaser 3 documentation itself. + * + * For Phaser 3 API Documentation, check: + * https://photonstorm.github.io/phaser3-docs/ + * + * @module game + * @author Anthony Halim + * @author Chi Xu + * @author Chong Sia Tiffany + * @author Gokul Rajiv + */ + +/* eslint-disable @typescript-eslint/no-unused-vars, consistent-return, @typescript-eslint/no-shadow, @typescript-eslint/default-param-last */ +import { + GameModuleParams, + GameObject, + List, + ObjectConfig, + RawContainer, + RawGameElement, + RawGameObject, + RawInputObject, +} from './types'; + +/** @hidden */ +export default function gameFuncs(moduleParams: GameModuleParams) { + const { + scene, + preloadImageMap, + preloadSoundMap, + preloadSpritesheetMap, + remotePath, + screenSize, + createAward, + } = moduleParams.game || {}; + + // Listener ObjectTypes + enum ListenerTypes { + InputPlugin = 'input_plugin', + KeyboardKeyType = 'keyboard_key', + } + + const ListnerTypes = Object.values(ListenerTypes); + + // Object ObjectTypes + enum ObjectTypes { + ImageType = 'image', + TextType = 'text', + RectType = 'rect', + EllipseType = 'ellipse', + ContainerType = 'container', + AwardType = 'award', + } + + const ObjTypes = Object.values(ObjectTypes); + + const nullFn = () => {}; + + // ============================================================================= + // Module's Private Functions + // ============================================================================= + + /** @hidden */ + function get_obj( + obj: GameObject, + ): RawGameObject | RawInputObject | RawContainer { + return obj.object!; + } + + /** @hidden */ + function get_game_obj(obj: GameObject): RawGameObject | RawContainer { + return obj.object as RawGameObject | RawContainer; + } + + /** @hidden */ + function get_input_obj(obj: GameObject): RawInputObject { + return obj.object as RawInputObject; + } + + /** @hidden */ + function get_container(obj: GameObject): RawContainer { + return obj.object as RawContainer; + } + + /** + * Checks whether the given game object is of the enquired type. + * If the given obj is undefined, will also return false. + * + * @param obj the game object + * @param type enquired type + * @returns if game object is of enquired type + * @hidden + */ + function is_type(obj: GameObject, type: string): boolean { + return obj !== undefined && obj.type === type && obj.object !== undefined; + } + + /** + * Checks whether the given game object is any of the enquired ObjectTypes + * + * @param obj the game object + * @param ObjectTypes enquired ObjectTypes + * @returns if game object is of any of the enquired ObjectTypes + * @hidden + */ + function is_any_type(obj: GameObject, types: string[]): boolean { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < types.length; ++i) { + if (is_type(obj, types[i])) return true; + } + return false; + } + + /** + * Set a game object to the given type. + * Mutates the object. + * + * @param object the game object + * @param type type to set + * @returns typed game object + * @hidden + */ + function set_type( + object: RawGameObject | RawInputObject | RawContainer, + type: string, + ): GameObject { + return { + type, + object, + }; + } + + /** + * Throw a console error, including the function caller name. + * + * @param {string} message error message + * @hidden + */ + function throw_error(message: string) { + // eslint-disable-next-line no-console, no-caller, @typescript-eslint/no-throw-literal, no-restricted-properties + throw console.error(`${arguments.callee.caller.name}: ${message}`); + } + + // List processing + // Original Author: Martin Henz + + /** + * array test works differently for Rhino and + * the Firefox environment (especially Web Console) + */ + function array_test(x: any): boolean { + if (Array.isArray === undefined) { + return x instanceof Array; + } + return Array.isArray(x); + } + + /** + * pair constructs a pair using a two-element array + * LOW-LEVEL FUNCTION, NOT SOURCE + */ + function pair(x: any, xs: any): [any, any] { + return [x, xs]; + } + + /** + * is_pair returns true iff arg is a two-element array + * LOW-LEVEL FUNCTION, NOT SOURCE + */ + function is_pair(x: any): boolean { + return array_test(x) && x.length === 2; + } + + /** + * head returns the first component of the given pair, + * throws an exception if the argument is not a pair + * LOW-LEVEL FUNCTION, NOT SOURCE + */ + function head(xs: List): any { + if (is_pair(xs)) { + return xs![0]; + } + throw new Error( + `head(xs) expects a pair as argument xs, but encountered ${xs}`, + ); + } + + /** + * tail returns the second component of the given pair + * throws an exception if the argument is not a pair + * LOW-LEVEL FUNCTION, NOT SOURCE + */ + function tail(xs: List) { + if (is_pair(xs)) { + return xs![1]; + } + throw new Error( + `tail(xs) expects a pair as argument xs, but encountered ${xs}`, + ); + } + + /** + * is_null returns true if arg is exactly null + * LOW-LEVEL FUNCTION, NOT SOURCE + */ + function is_null(xs: any) { + return xs === null; + } + + /** + * map applies first arg f to the elements of the second argument, + * assumed to be a list. + * f is applied element-by-element: + * map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] + * map throws an exception if the second argument is not a list, + * and if the second argument is a non-empty list and the first + * argument is not a function. + */ + function map(f: (x: any) => any, xs: List) { + return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); + } + + // ============================================================================= + // Module's Exposed Functions + // ============================================================================= + + // HELPER + + function prepend_remote_url(asset_key: string): string { + return remotePath(asset_key); + } + + function create_config(lst: List): ObjectConfig { + const config = {}; + map((xs: [any, any]) => { + if (!is_pair(xs)) { + throw_error('xs is not pair!'); + } + config[head(xs)] = tail(xs); + }, lst); + return config; + } + + function create_text_config( + font_family: string = 'Courier', + font_size: string = '16px', + color: string = '#fff', + stroke: string = '#fff', + stroke_thickness: number = 0, + align: string = 'left', + ): ObjectConfig { + return { + fontFamily: font_family, + fontSize: font_size, + color, + stroke, + strokeThickness: stroke_thickness, + align, + }; + } + + function create_interactive_config( + draggable: boolean = false, + use_hand_cursor: boolean = false, + pixel_perfect: boolean = false, + alpha_tolerance: number = 1, + ): ObjectConfig { + return { + draggable, + useHandCursor: use_hand_cursor, + pixelPerfect: pixel_perfect, + alphaTolerance: alpha_tolerance, + }; + } + + function create_sound_config( + mute: boolean = false, + volume: number = 1, + rate: number = 1, + detune: number = 0, + seek: number = 0, + loop: boolean = false, + delay: number = 0, + ): ObjectConfig { + return { + mute, + volume, + rate, + detune, + seek, + loop, + delay, + }; + } + + function create_tween_config( + target_prop: string = 'x', + target_value: string | number = 0, + delay: number = 0, + duration: number = 1000, + ease: Function | string = 'Power0', + on_complete: Function = nullFn, + yoyo: boolean = false, + loop: number = 0, + loop_delay: number = 0, + on_loop: Function = nullFn, + ): ObjectConfig { + return { + [target_prop]: target_value, + delay, + duration, + ease, + onComplete: on_complete, + yoyo, + loop, + loopDelay: loop_delay, + onLoop: on_loop, + }; + } + + function create_anim_config( + anims_key: string, + anim_frames: ObjectConfig[], + frame_rate: number = 24, + duration: any = null, + repeat: number = -1, + yoyo: boolean = false, + show_on_start: boolean = true, + hide_on_complete: boolean = false, + ): ObjectConfig { + return { + key: anims_key, + frames: anim_frames, + frameRate: frame_rate, + duration, + repeat, + yoyo, + showOnStart: show_on_start, + hideOnComplete: hide_on_complete, + }; + } + + function create_anim_frame_config( + key: string, + duration: number = 0, + visible: boolean = true, + ): ObjectConfig { + return { + key, + duration, + visible, + }; + } + + function create_anim_spritesheet_frame_configs( + key: string, + ): ObjectConfig[] | undefined { + if (preloadSpritesheetMap.get(key)) { + const configArr = scene.anims.generateFrameNumbers(key, {}); + return configArr; + } + throw_error(`${key} is not associated with any spritesheet`); + } + + function create_spritesheet_config( + frame_width: number, + frame_height: number, + start_frame: number = 0, + margin: number = 0, + spacing: number = 0, + ): ObjectConfig { + return { + frameWidth: frame_width, + frameHeight: frame_height, + startFrame: start_frame, + margin, + spacing, + }; + } + + // SCREEN + + function get_screen_width(): number { + return screenSize.x; + } + + function get_screen_height(): number { + return screenSize.y; + } + + function get_screen_display_width(): number { + return scene.scale.displaySize.width; + } + + function get_screen_display_height(): number { + return scene.scale.displaySize.height; + } + + // LOAD + + function load_image(key: string, url: string) { + preloadImageMap.set(key, url); + } + + function load_sound(key: string, url: string) { + preloadSoundMap.set(key, url); + } + + function load_spritesheet( + key: string, + url: string, + spritesheet_config: ObjectConfig, + ) { + preloadSpritesheetMap.set(key, [url, spritesheet_config]); + } + + // ADD + + function add(obj: GameObject): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + scene.add.existing(get_game_obj(obj)); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + // SOUND + + function play_sound(key: string, config: ObjectConfig = {}): void { + if (preloadSoundMap.get(key)) { + scene.sound.play(key, config); + } else { + throw_error(`${key} is not associated with any sound`); + } + } + + // ANIMS + + function create_anim(anim_config: ObjectConfig): boolean { + const anims = scene.anims.create(anim_config); + return typeof anims !== 'boolean'; + } + + function play_anim_on_image( + image: GameObject, + anims_key: string, + ): GameObject | undefined { + if (is_type(image, ObjectTypes.ImageType)) { + (get_obj(image) as Phaser.GameObjects.Sprite).play(anims_key); + return image; + } + throw_error(`${image} is not of type ${ObjectTypes.ImageType}`); + } + + // IMAGE + + function create_image( + x: number, + y: number, + asset_key: string, + ): GameObject | undefined { + if ( + preloadImageMap.get(asset_key) + || preloadSpritesheetMap.get(asset_key) + ) { + const image = new Phaser.GameObjects.Sprite(scene, x, y, asset_key); + return set_type(image, ObjectTypes.ImageType); + } + throw_error(`${asset_key} is not associated with any image`); + } + + // AWARD + + function create_award(x: number, y: number, award_key: string): GameObject { + return set_type(createAward(x, y, award_key), ObjectTypes.AwardType); + } + + // TEXT + + function create_text( + x: number, + y: number, + text: string, + config: ObjectConfig = {}, + ): GameObject { + const txt = new Phaser.GameObjects.Text(scene, x, y, text, config); + return set_type(txt, ObjectTypes.TextType); + } + + // RECTANGLE + + function create_rect( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, + ): GameObject { + const rect = new Phaser.GameObjects.Rectangle( + scene, + x, + y, + width, + height, + fill, + alpha, + ); + return set_type(rect, ObjectTypes.RectType); + } + + // ELLIPSE + + function create_ellipse( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, + ): GameObject { + const ellipse = new Phaser.GameObjects.Ellipse( + scene, + x, + y, + width, + height, + fill, + alpha, + ); + return set_type(ellipse, ObjectTypes.EllipseType); + } + + // CONTAINER + + function create_container(x: number, y: number): GameObject { + const cont = new Phaser.GameObjects.Container(scene, x, y); + return set_type(cont, ObjectTypes.ContainerType); + } + + function add_to_container( + container: GameObject, + obj: GameObject, + ): GameObject | undefined { + if ( + is_type(container, ObjectTypes.ContainerType) + && is_any_type(obj, ObjTypes) + ) { + get_container(container) + .add(get_game_obj(obj)); + return container; + } + throw_error( + `${obj} is not of type ${ObjTypes} or ${container} is not of type ${ObjectTypes.ContainerType}`, + ); + } + + // OBJECT + + function destroy_obj(obj: GameObject) { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .destroy(); + } else { + throw_error(`${obj} is not of type ${ObjTypes}`); + } + } + + function set_display_size( + obj: GameObject, + x: number, + y: number, + ): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setDisplaySize(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_alpha(obj: GameObject, alpha: number): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setAlpha(alpha); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_interactive( + obj: GameObject, + config: ObjectConfig = {}, + ): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setInteractive(config); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_origin( + obj: GameObject, + x: number, + y: number, + ): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + (get_game_obj(obj) as RawGameObject).setOrigin(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_position( + obj: GameObject, + x: number, + y: number, + ): GameObject | undefined { + if (obj && is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setPosition(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_scale( + obj: GameObject, + x: number, + y: number, + ): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setScale(x, y); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_rotation(obj: GameObject, rad: number): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + get_game_obj(obj) + .setRotation(rad); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function set_flip( + obj: GameObject, + x: boolean, + y: boolean, + ): GameObject | undefined { + const GameElementType = [ObjectTypes.ImageType, ObjectTypes.TextType]; + if (is_any_type(obj, GameElementType)) { + (get_obj(obj) as RawGameElement).setFlip(x, y); + return obj; + } + throw_error(`${obj} is not of type ${GameElementType}`); + } + + async function add_tween( + obj: GameObject, + config: ObjectConfig = {}, + ): Promise { + if (is_any_type(obj, ObjTypes)) { + scene.tweens.add({ + targets: get_game_obj(obj), + ...config, + }); + return obj; + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + // LISTENER + + function add_listener( + obj: GameObject, + event: string, + callback: Function, + ): GameObject | undefined { + if (is_any_type(obj, ObjTypes)) { + const listener = get_game_obj(obj) + .addListener(event, callback); + return set_type(listener, ListenerTypes.InputPlugin); + } + throw_error(`${obj} is not of type ${ObjTypes}`); + } + + function add_keyboard_listener( + key: string | number, + event: string, + callback: Function, + ): GameObject { + const keyObj = scene.input.keyboard.addKey(key); + const keyboardListener = keyObj.addListener(event, callback); + return set_type(keyboardListener, ListenerTypes.KeyboardKeyType); + } + + function remove_listener(listener: GameObject): boolean { + if (is_any_type(listener, ListnerTypes)) { + get_input_obj(listener) + .removeAllListeners(); + return true; + } + return false; + } + + const functions = { + add, + add_listener, + add_keyboard_listener, + add_to_container, + add_tween, + create_anim, + create_anim_config, + create_anim_frame_config, + create_anim_spritesheet_frame_configs, + create_award, + create_config, + create_container, + create_ellipse, + create_image, + create_interactive_config, + create_rect, + create_text, + create_text_config, + create_tween_config, + create_sound_config, + create_spritesheet_config, + destroy_obj, + get_screen_width, + get_screen_height, + get_screen_display_width, + get_screen_display_height, + load_image, + load_sound, + load_spritesheet, + play_anim_on_image, + play_sound, + prepend_remote_url, + remove_listener, + set_alpha, + set_display_size, + set_flip, + set_interactive, + set_origin, + set_position, + set_rotation, + set_scale, + }; + + const finalFunctions = {}; + + // eslint-disable-next-line array-callback-return + Object.entries(functions) + .map(([key, fn]) => { + finalFunctions[key] = !scene ? nullFn : fn; + }); + + return finalFunctions; +} + +// ============================================================================= +// Dummy functions for TypeDoc +// +// Refer to functions of matching signature in `gameFuncs` +// for implementation details +// ============================================================================= + +/** + * Prepend the given asset key with the remote path (S3 path). + * + * @param asset_key + * @returns prepended path + */ +export function prepend_remote_url(asset_key: string): string { + return ''; +} + +/** + * Transforms the given list into an object config. The list follows + * the format of list([key1, value1], [key2, value2]). + * + * e.g list(["alpha", 0], ["duration", 1000]) + * + * @param lst the list to be turned into object config. + * @returns object config + */ +export function create_config(lst: List): ObjectConfig { + return {}; +} + +/** + * Create text config object, can be used to stylise text object. + * + * font_family: for available font_family, see: + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Valid_family_names + * + * align: must be either 'left', 'right', 'center', or 'justify' + * + * For more details about text config, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.GameObjects.Text.html#.TextStyle + * + * @param font_family font to be used + * @param font_size size of font, must be appended with 'px' e.g. '16px' + * @param color colour of font, in hex e.g. '#fff' + * @param stroke colour of stroke, in hex e.g. '#fff' + * @param stroke_thickness thickness of stroke + * @param align text alignment + * @returns text config + */ +export function create_text_config( + font_family: string = 'Courier', + font_size: string = '16px', + color: string = '#fff', + stroke: string = '#fff', + stroke_thickness: number = 0, + align: string = 'left', +): ObjectConfig { + return {}; +} + +/** + * Create interactive config object, can be used to configure interactive settings. + * + * For more details about interactive config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Input.html#.InputConfiguration + * + * @param draggable object will be set draggable + * @param use_hand_cursor if true, pointer will be set to 'pointer' when a pointer is over it + * @param pixel_perfect pixel perfect function will be set for the hit area. Only works for texture based object + * @param alpha_tolerance if pixel_perfect is set, this is the alpha tolerance threshold value used in the callback + * @returns interactive config + */ +export function create_interactive_config( + draggable: boolean = false, + use_hand_cursor: boolean = false, + pixel_perfect: boolean = false, + alpha_tolerance: number = 1, +): ObjectConfig { + return {}; +} + +/** + * Create sound config object, can be used to configure sound settings. + * + * For more details about sound config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Sound.html#.SoundConfig + * + * @param mute whether the sound should be muted or not + * @param volume value between 0(silence) and 1(full volume) + * @param rate the speed at which the sound is played + * @param detune detuning of the sound, in cents + * @param seek position of playback for the sound, in seconds + * @param loop whether or not the sound should loop + * @param delay time, in seconds, that elapse before the sound actually starts + * @returns sound config + */ +export function create_sound_config( + mute: boolean = false, + volume: number = 1, + rate: number = 1, + detune: number = 0, + seek: number = 0, + loop: boolean = false, + delay: number = 0, +): ObjectConfig { + return {}; +} + +/** + * Create tween config object, can be used to configure tween settings. + * + * For more details about tween config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Tweens.html#.TweenBuilderConfig + * + * @param target_prop target to tween, e.g. x, y, alpha + * @param target_value the property value to tween to + * @param delay time in ms/frames before tween will start + * @param duration duration of tween in ms/frames, exclude yoyos or repeats + * @param ease ease function to use, e.g. 'Power0', 'Power1', 'Power2' + * @param on_complete function to execute when tween completes + * @param yoyo if set to true, once tween complete, reverses the values incrementally to get back to the starting tween values + * @param loop number of times the tween should loop, or -1 to loop indefinitely + * @param loop_delay The time the tween will pause before starting either a yoyo or returning to the start for a repeat + * @param on_loop function to execute each time the tween loops + * @returns tween config + */ +export function create_tween_config( + target_prop: string = 'x', + target_value: string | number = 0, + delay: number = 0, + duration: number = 1000, + ease: Function | string = 'Power0', + on_complete: Function, + yoyo: boolean = false, + loop: number = 0, + loop_delay: number = 0, + on_loop: Function, +): ObjectConfig { + return {}; +} + +/** + * Create anims config, can be used to configure anims + * + * For more details about the config object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Types.Animations.html#.Animation + * + * @param anims_key key that the animation will be associated with + * @param anim_frames data used to generate the frames for animation + * @param frame_rate frame rate of playback in frames per second + * @param duration how long the animation should play in seconds. + * If null, will be derived from frame_rate + * @param repeat number of times to repeat the animation, -1 for infinity + * @param yoyo should the animation yoyo (reverse back down to the start) + * @param show_on_start should the sprite be visible when the anims start? + * @param hide_on_complete should the sprite be not visible when the anims finish? + * @returns animation config + */ +export function create_anim_config( + anims_key: string, + anim_frames: ObjectConfig[], + frame_rate: number = 24, + duration: any = null, + repeat: number = -1, + yoyo: boolean = false, + show_on_start: boolean = true, + hide_on_complete: boolean = false, +): ObjectConfig { + return {}; +} + +/** + * Create animation frame config, can be used to configure a specific frame + * within an animation. + * + * The key should refer to an image that is already loaded. + * To make frame_config from spritesheet based on its frames, + * use create_anim_spritesheet_frame_configs instead. + * + * @param key key that is associated with the sprite at this frame + * @param duration duration, in ms, of this frame of the animation + * @param visible should the parent object be visible during this frame? + * @returns animation frame config + */ +export function create_anim_frame_config( + key: string, + duration: number = 0, + visible: boolean = true, +): ObjectConfig { + return {}; +} + +/** + * Create list of animation frame config, can be used directly as part of + * anim_config's `frames` parameter. + * + * This function will generate list of frame configs based on the + * spritesheet_config attached to the associated spritesheet. + * This function requires that the given key is a spritesheet key + * i.e. a key associated with loaded spritesheet, loaded in using + * load_spritesheet function. + * + * Will return empty frame configs if key is not associated with + * a spritesheet. + * + * @param key key associated with spritesheet + * @returns animation frame configs + */ +export function create_anim_spritesheet_frame_configs( + key: string, +): ObjectConfig[] | undefined { + return undefined; +} + +/** + * Create spritesheet config, can be used to configure the frames within the + * spritesheet. Can be used as config at load_spritesheet. + * + * @param frame_width width of frame in pixels + * @param frame_height height of frame in pixels + * @param start_frame first frame to start parsing from + * @param margin margin in the image; this is the space around the edge of the frames + * @param spacing the spacing between each frame in the image + * @returns spritesheet config + */ +export function create_spritesheet_config( + frame_width: number, + frame_height: number, + start_frame: number = 0, + margin: number = 0, + spacing: number = 0, +): ObjectConfig { + return {}; +} + +// SCREEN + +/** + * Get in-game screen width. + * + * @return screen width + */ +export function get_screen_width(): number { + return -1; +} + +/** + * Get in-game screen height. + * + * @return screen height + */ +export function get_screen_height(): number { + return -1; +} + +/** + * Get game screen display width (accounting window size). + * + * @return screen display width + */ +export function get_screen_display_width(): number { + return -1; +} + +/** + * Get game screen display height (accounting window size). + * + * @return screen display height + */ +export function get_screen_display_height(): number { + return -1; +} + +// LOAD + +/** + * Load the image asset into the scene for use. All images + * must be loaded before used in create_image. + * + * @param key key to be associated with the image + * @param url path to the image + */ +export function load_image(key: string, url: string) {} + +/** + * Load the sound asset into the scene for use. All sound + * must be loaded before used in play_sound. + * + * @param key key to be associated with the sound + * @param url path to the sound + */ +export function load_sound(key: string, url: string) {} + +/** + * Load the spritesheet into the scene for use. All spritesheet must + * be loaded before used in create_image. + * + * @param key key associated with the spritesheet + * @param url path to the sound + * @param spritesheet_config config to determines frames within the spritesheet + */ +export function load_spritesheet( + key: string, + url: string, + spritesheet_config: ObjectConfig, +) { + return {}; +} + +// ADD + +/** + * Add the object to the scene. Only objects added to the scene + * will appear. + * + * @param obj game object to be added + */ +export function add(obj: GameObject): GameObject | undefined { + return undefined; +} + +// SOUND + +/** + * Play the sound associated with the key. + * Throws error if key is non-existent. + * + * @param key key to the sound to be played + * @param config sound config to be used + */ +export function play_sound(key: string, config: ObjectConfig = {}): void {} + +// ANIMS + +/** + * Create a new animation and add it to the available animations. + * Animations are global i.e. once created, it can be used anytime, anywhere. + * + * NOTE: Anims DO NOT need to be added into the scene to be used. + * It is automatically added to the scene when it is created. + * + * Will return true if the animation key is valid + * (key is specified within the anim_config); false if the key + * is already in use. + * + * @param anim_config + * @returns true if animation is successfully created, false otherwise + */ +export function create_anim(anim_config: ObjectConfig): boolean { + return false; +} + +/** + * Start playing the given animation on image game object. + * + * @param image image game object + * @param anims_key key associated with an animation + */ +export function play_anim_on_image( + image: GameObject, + anims_key: string, +): GameObject | undefined { + return undefined; +} + +// IMAGE + +/** + * Create an image using the key associated with a loaded image. + * If key is not associated with any loaded image, throws error. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x position of the image. 0 is at the left side + * @param y y position of the image. 0 is at the top side + * @param asset_key key to loaded image + * @returns image game object + */ +export function create_image( + x: number, + y: number, + asset_key: string, +): GameObject | undefined { + return undefined; +} + +// AWARD + +/** + * Create an award using the key associated with the award. + * The award key can be obtained from the Awards Hall or + * Awards menu, after attaining the award. + * + * Valid award will have an on-hover VERIFIED tag to distinguish + * it from images created by create_image. + * + * If student does not possess the award, this function will + * return a untagged, default image. + * + * @param x x position of the image. 0 is at the left side + * @param y y position of the image. 0 is at the top side + * @param award_key key for award + * @returns award game object + */ +export function create_award( + x: number, + y: number, + award_key: string, +): GameObject { + return { + type: 'null', + object: undefined, + }; +} + +// TEXT + +/** + * Create a text object. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x position of the text + * @param y y position of the text + * @param text text to be shown + * @param config text configuration to be used + * @returns text game object + */ +export function create_text( + x: number, + y: number, + text: string, + config: ObjectConfig = {}, +): GameObject { + return { + type: 'null', + object: undefined, + }; +} + +// RECTANGLE + +/** + * Create a rectangle object. + * + * 0, 0 is located at the top, left hand side. + * + * @param x x coordinate of the top, left corner posiiton + * @param y y coordinate of the top, left corner position + * @param width width of rectangle + * @param height height of rectangle + * @param fill colour fill, in hext e.g 0xffffff + * @param alpha value between 0 and 1 to denote alpha + * @returns rectangle object + */ +export function create_rect( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, +): GameObject { + return { + type: 'null', + object: undefined, + }; +} + +// ELLIPSE + +/** + * Create an ellipse object. + * + * @param x x coordinate of the centre of ellipse + * @param y y coordinate of the centre of ellipse + * @param width width of ellipse + * @param height height of ellipse + * @param fill colour fill, in hext e.g 0xffffff + * @param alpha value between 0 and 1 to denote alpha + * @returns ellipse object + */ +export function create_ellipse( + x: number, + y: number, + width: number, + height: number, + fill: number = 0, + alpha: number = 1, +): GameObject { + return { + type: 'null', + object: undefined, + }; +} + +// CONTAINER + +/** + * Create a container object. Container is able to contain any other game object, + * and the positions of contained game object will be relative to the container. + * + * Rendering the container as visible or invisible will also affect the contained + * game object. + * + * Container can also contain another container. + * + * 0, 0 is located at the top, left hand side. + * + * For more details about container object, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.GameObjects.Container.html + * + * @param x x position of the container + * @param y y position of the container + * @returns container object + */ +export function create_container(x: number, y: number): GameObject { + return { + type: 'container', + object: undefined, + }; +} + +/** + * Add the given game object to the container. + * Mutates the container. + * + * @param container container object + * @param obj game object to add to the container + * @returns container object + */ +export function add_to_container( + container: GameObject, + obj: GameObject, +): GameObject | undefined { + return undefined; +} + +// OBJECT + +/** + * Destroy the given game object. Destroyed game object + * is removed from the scene, and all of its listeners + * is also removed. + * + * @param obj game object itself + */ +export function destroy_obj(obj: GameObject) {} + +/** + * Set the display size of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new display width size + * @param y new display height size + * @returns game object itself + */ +export function set_display_size( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + return undefined; +} + +/** + * Set the alpha of the object. + * Mutate the object. + * + * @param obj object to be set + * @param alpha new alpha + * @returns game object itself + */ +export function set_alpha( + obj: GameObject, + alpha: number, +): GameObject | undefined { + return undefined; +} + +/** + * Set the interactivity of the object. + * Mutate the object. + * + * Rectangle and Ellipse are not able to receive configs, only boolean + * i.e. set_interactive(rect, true); set_interactive(ellipse, false) + * + * @param obj object to be set + * @param config interactive config to be used + * @returns game object itself + */ +export function set_interactive( + obj: GameObject, + config: ObjectConfig = {}, +): GameObject | undefined { + return undefined; +} + +/** + * Set the origin in which all position related will be relative to. + * In other words, the anchor of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new anchor x coordinate, between value 0 to 1. + * @param y new anchor y coordinate, between value 0 to 1. + * @returns game object itself + */ +export function set_origin( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + return undefined; +} + +/** + * Set the position of the game object + * Mutate the object + * + * @param obj object to be set + * @param x new x position + * @param y new y position + * @returns game object itself + */ +export function set_position( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + return undefined; +} + +/** + * Set the scale of the object. + * Mutate the object. + * + * @param obj object to be set + * @param x new x scale + * @param y new y scale + * @returns game object itself + */ +export function set_scale( + obj: GameObject, + x: number, + y: number, +): GameObject | undefined { + return undefined; +} + +/** + * Set the rotation of the object. + * Mutate the object. + * + * @param obj object to be set + * @param rad the rotation, in radians + * @returns game object itself + */ +export function set_rotation( + obj: GameObject, + rad: number, +): GameObject | undefined { + return undefined; +} + +/** + * Sets the horizontal and flipped state of the object. + * Mutate the object. + * + * @param obj game object itself + * @param x to flip in the horizontal state + * @param y to flip in the vertical state + * @returns game object itself + */ +export function set_flip( + obj: GameObject, + x: boolean, + y: boolean, +): GameObject | undefined { + return undefined; +} + +/** + * Creates a tween to the object and plays it. + * Mutate the object. + * + * @param obj object to be added to + * @param config tween config + * @returns game object itself + */ +export async function add_tween( + obj: GameObject, + config: ObjectConfig = {}, +): Promise { + return undefined; +} + +// LISTENER + +/** + * Attach a listener to the object. The callback will be executed + * when the event is emitted. + * Mutate the object. + * + * For all available events, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html + * + * @param obj object to be added to + * @param event the event name + * @param callback listener function, executed on event + * @returns listener game object + */ +export function add_listener( + obj: GameObject, + event: string, + callback: Function, +): GameObject | undefined { + return undefined; +} + +/** + * Attach a listener to the object. The callback will be executed + * when the event is emitted. + * Mutate the object. + * + * For all available events, see: + * https://photonstorm.github.io/phaser3-docs/Phaser.Input.Events.html + * + * For list of keycodes, see: + * https://github.com/photonstorm/phaser/blob/v3.22.0/src/input/keyboard/keys/KeyCodes.js + * + * @param key keyboard key to trigger listener + * @param event the event name + * @param callback listener function, executed on event + * @returns listener game object + */ +export function add_keyboard_listener( + key: string | number, + event: string, + callback: Function, +): GameObject { + return { + type: 'null', + object: undefined, + }; +} + +/** + * Deactivate and remove listener. + * + * @param listener + * @returns if successful + */ +export function remove_listener(listener: GameObject): boolean { + return false; +} diff --git a/src/bundles/game/index.ts b/src/bundles/game/index.ts index 9474b6573..d7d35f992 100644 --- a/src/bundles/game/index.ts +++ b/src/bundles/game/index.ts @@ -1,8 +1,8 @@ -import { ModuleContexts } from '../../typings/type_helpers.js'; -import gameFuncs from './functions'; -import { GameModuleParams } from './types.js'; - -export default ( - moduleParams: GameModuleParams, - _moduleContexts: ModuleContexts -) => gameFuncs(moduleParams); +import { ModuleContexts } from '../../typings/type_helpers.js'; +import gameFuncs from './functions'; +import { GameModuleParams } from './types.js'; + +export default ( + moduleParams: GameModuleParams, + _moduleContexts: ModuleContexts, +) => gameFuncs(moduleParams); diff --git a/src/bundles/game/types.ts b/src/bundles/game/types.ts index 36362f48c..968a6a75a 100644 --- a/src/bundles/game/types.ts +++ b/src/bundles/game/types.ts @@ -1,41 +1,41 @@ -import * as Phaser from 'phaser'; -import { ModuleParams } from '../../typings/type_helpers.js'; - -export type List = [any, List] | null; - -export type ObjectConfig = { [attr: string]: any }; - -export type RawGameElement = - | Phaser.GameObjects.Sprite - | Phaser.GameObjects.Text; - -export type RawGameShape = - | Phaser.GameObjects.Rectangle - | Phaser.GameObjects.Ellipse; - -export type RawGameObject = RawGameElement | RawGameShape; - -export type RawContainer = Phaser.GameObjects.Container; - -export type RawInputObject = - | Phaser.Input.InputPlugin - | Phaser.Input.Keyboard.Key; - -export type GameObject = { - type: string; - object: RawGameObject | RawInputObject | RawContainer | undefined; -}; - -export type GameModuleParams = ModuleParams & { - game: GameParams; -}; - -export type GameParams = { - scene: Phaser.Scene; - preloadImageMap: Map; - preloadSoundMap: Map; - preloadSpritesheetMap: Map; - remotePath: (path: string) => string; - screenSize: { x: number; y: number }; - createAward: (x: number, y: number, key: string) => Phaser.GameObjects.Sprite; -}; +import * as Phaser from 'phaser'; +import { ModuleParams } from '../../typings/type_helpers.js'; + +export type List = [any, List] | null; + +export type ObjectConfig = { [attr: string]: any }; + +export type RawGameElement = + | Phaser.GameObjects.Sprite + | Phaser.GameObjects.Text; + +export type RawGameShape = + | Phaser.GameObjects.Rectangle + | Phaser.GameObjects.Ellipse; + +export type RawGameObject = RawGameElement | RawGameShape; + +export type RawContainer = Phaser.GameObjects.Container; + +export type RawInputObject = + | Phaser.Input.InputPlugin + | Phaser.Input.Keyboard.Key; + +export type GameObject = { + type: string; + object: RawGameObject | RawInputObject | RawContainer | undefined; +}; + +export type GameModuleParams = ModuleParams & { + game: GameParams; +}; + +export type GameParams = { + scene: Phaser.Scene; + preloadImageMap: Map; + preloadSoundMap: Map; + preloadSpritesheetMap: Map; + remotePath: (path: string) => string; + screenSize: { x: number; y: number }; + createAward: (x: number, y: number, key: string) => Phaser.GameObjects.Sprite; +}; diff --git a/src/bundles/mark_sweep/index.ts b/src/bundles/mark_sweep/index.ts index afff55611..dae23adbb 100644 --- a/src/bundles/mark_sweep/index.ts +++ b/src/bundles/mark_sweep/index.ts @@ -56,7 +56,7 @@ function initialize_memory( memorySize: number, nodeSize, marked, - unmarked + unmarked, ): void { MEMORY_SIZE = memorySize; NODE_SIZE = nodeSize; @@ -91,7 +91,7 @@ function newCommand( description, firstDesc, lastDesc, - queue = [] + queue = [], ): void { const newType = type; const newLeft = left; @@ -139,7 +139,7 @@ function newSweep(left, heap): void { heap, desc, 'freed node', - '' + '', ); } @@ -156,7 +156,7 @@ function newMark(left, heap, queue): void { desc, 'marked node', '', - queue + queue, ); } @@ -167,7 +167,7 @@ function addRoots(arr): void { } function showRoot(heap): void { - const desc = `All root nodes are marked`; + const desc = 'All root nodes are marked'; newCommand(COMMAND.SHOW_MARKED, -1, -1, 0, 0, heap, desc, '', ''); } @@ -189,7 +189,7 @@ function newUpdateSweep(right, heap): void { heap, desc, 'free node', - '' + '', ); } @@ -204,7 +204,7 @@ function newPush(left, right, heap): void { heap, desc, 'last child address slot', - 'new child pushed' + 'new child pushed', ); } @@ -220,7 +220,7 @@ function newPop(res, left, right, heap): void { heap, desc, 'popped memory', - 'last child address slot' + 'last child address slot', ); } @@ -242,18 +242,18 @@ function newNew(left, heap): void { heap, desc, 'new memory allocated', - '' + '', ); } function newGC(heap): void { - const desc = `Memory exhausted, start Mark and Sweep Algorithm`; + const desc = 'Memory exhausted, start Mark and Sweep Algorithm'; newCommand(COMMAND.START, -1, -1, 0, 0, heap, desc, '', ''); updateFlip(); } function endGC(heap): void { - const desc = `Result of free memory`; + const desc = 'Result of free memory'; newCommand(COMMAND.END, -1, -1, 0, 0, heap, desc, '', ''); updateFlip(); } @@ -262,7 +262,7 @@ function updateSlotSegment( tag: number, size: number, first: number, - last: number + last: number, ): void { if (tag >= 0) { TAG_SLOT = tag; diff --git a/src/bundles/mark_sweep/types.ts b/src/bundles/mark_sweep/types.ts index 56c71389d..8d30ca24a 100644 --- a/src/bundles/mark_sweep/types.ts +++ b/src/bundles/mark_sweep/types.ts @@ -1,32 +1,32 @@ -export type Memory = number[]; -export type MemoryHeaps = Memory[]; -export type Tag = number; - -export enum COMMAND { - FLIP = 'Flip', - PUSH = 'Push', - POP = 'Pop', - COPY = 'Copy', - ASSIGN = 'Assign', - NEW = 'New', - START = 'Mark and Sweep Start', - END = 'End of Garbage Collector', - RESET = 'Sweep Reset', - SHOW_MARKED = 'Marked Roots', - MARK = 'Mark', - SWEEP = 'Sweep', - INIT = 'Initialize Memory', -} - -export type CommandHeapObject = { - type: String; - heap: number[]; - left: number; - right: number; - sizeLeft: number; - sizeRight: number; - desc: String; - leftDesc: String; - rightDesc: String; - queue: number[]; -}; +export type Memory = number[]; +export type MemoryHeaps = Memory[]; +export type Tag = number; + +export enum COMMAND { + FLIP = 'Flip', + PUSH = 'Push', + POP = 'Pop', + COPY = 'Copy', + ASSIGN = 'Assign', + NEW = 'New', + START = 'Mark and Sweep Start', + END = 'End of Garbage Collector', + RESET = 'Sweep Reset', + SHOW_MARKED = 'Marked Roots', + MARK = 'Mark', + SWEEP = 'Sweep', + INIT = 'Initialize Memory', +} + +export type CommandHeapObject = { + type: String; + heap: number[]; + left: number; + right: number; + sizeLeft: number; + sizeRight: number; + desc: String; + leftDesc: String; + rightDesc: String; + queue: number[]; +}; diff --git a/src/bundles/pix_n_flix/constants.ts b/src/bundles/pix_n_flix/constants.ts index bba15c914..94c11c55f 100644 --- a/src/bundles/pix_n_flix/constants.ts +++ b/src/bundles/pix_n_flix/constants.ts @@ -1,13 +1,13 @@ -// Default values of video -export const DEFAULT_WIDTH: number = 400; -export const DEFAULT_HEIGHT: number = 300; -export const DEFAULT_FPS: number = 10; -export const DEFAULT_VOLUME: number = 0.5; - -// Maximum values allowed for video -export const MAX_HEIGHT: number = 500; -export const MIN_HEIGHT: number = 1; -export const MAX_WIDTH: number = 500; -export const MIN_WIDTH: number = 1; -export const MAX_FPS: number = 30; -export const MIN_FPS: number = 2; +// Default values of video +export const DEFAULT_WIDTH: number = 400; +export const DEFAULT_HEIGHT: number = 300; +export const DEFAULT_FPS: number = 10; +export const DEFAULT_VOLUME: number = 0.5; + +// Maximum values allowed for video +export const MAX_HEIGHT: number = 500; +export const MIN_HEIGHT: number = 1; +export const MAX_WIDTH: number = 500; +export const MIN_WIDTH: number = 1; +export const MAX_FPS: number = 30; +export const MIN_FPS: number = 2; diff --git a/src/bundles/pix_n_flix/functions.ts b/src/bundles/pix_n_flix/functions.ts index 395398223..ebc551cb9 100644 --- a/src/bundles/pix_n_flix/functions.ts +++ b/src/bundles/pix_n_flix/functions.ts @@ -1,725 +1,732 @@ -/** - * The pix_n_flix module allows us to process still images and videos. - * - * An Image (which is a still image or a frame of a video) is a - * two-dimensional array of Pixels, and a Pixel consists of red, blue and green color - * values, each ranging from 0 to 255. To access these color values of a Pixel, we - * provide the functions red_of, blue_of and green_of. - * - * A central element of pix_n_flix is the notion of a Filter, a function that is applied - * to two Images: the source Image and the destination Image. When a Filter is installed - * (using the function install_filter), it transforms each source Image from the live camera - * or from a local/remote file to a destination Image that is then displayed on screen - * in the Source Academy "Pix N Flix" tab (with a camera icon). - * - * The dimensions (i.e. width and height) of the displayed images can be set by the user using - * the function set_dimensions, and all source and destination Images of the Filters will - * also be set to the same dimensions. To access the current dimensions of the Images, the user - * can use the functions image_width and image_height. - * - * @module pix_n_flix - */ - -/* eslint-disable @typescript-eslint/no-shadow */ -import { - CanvasElement, - VideoElement, - ErrorLogger, - StartPacket, - Pixel, - Pixels, - Filter, - Queue, - TabsPacket, - BundlePacket, - InputFeed, - ImageElement, -} from './types'; - -import { - DEFAULT_WIDTH, - DEFAULT_HEIGHT, - DEFAULT_FPS, - DEFAULT_VOLUME, - MAX_HEIGHT, - MIN_HEIGHT, - MAX_WIDTH, - MIN_WIDTH, - MAX_FPS, - MIN_FPS, -} from './constants'; - -// Global Variables -let WIDTH: number = DEFAULT_WIDTH; -let HEIGHT: number = DEFAULT_HEIGHT; -let FPS: number = DEFAULT_FPS; -let VOLUME: number = DEFAULT_VOLUME; - -let imageElement: ImageElement; -let videoElement: VideoElement; -let canvasElement: CanvasElement; -let canvasRenderingContext: CanvasRenderingContext2D; -let errorLogger: ErrorLogger; -let tabsPackage: TabsPacket; - -const pixels: Pixels = []; -const temporaryPixels: Pixels = []; -// eslint-disable-next-line @typescript-eslint/no-use-before-define -let filter: Filter = copy_image; - -let toRunLateQueue: boolean = false; -let videoIsPlaying: boolean = false; - -let requestId: number; -let prevTime: number | null = null; -let totalElapsedTime: number = 0; - -let inputFeed: InputFeed = InputFeed.Camera; -let url: string = ''; - -// ============================================================================= -// Module's Private Functions -// ============================================================================= - -/** @hidden */ -function setupData(): void { - for (let i = 0; i < HEIGHT; i += 1) { - pixels[i] = []; - temporaryPixels[i] = []; - for (let j = 0; j < WIDTH; j += 1) { - pixels[i][j] = [0, 0, 0, 255]; - temporaryPixels[i][j] = [0, 0, 0, 255]; - } - } -} - -/** @hidden */ -function isPixelFilled(pixel: Pixel): boolean { - let ok = true; - for (let i = 0; i < 4; i += 1) { - if (pixel[i] >= 0 && pixel[i] <= 255) { - // eslint-disable-next-line no-continue - continue; - } - ok = false; - // eslint-disable-next-line no-param-reassign - pixel[i] = 0; - } - return ok; -} - -/** @hidden */ -function writeToBuffer(buffer: Uint8ClampedArray, data: Pixels) { - let ok: boolean = true; - - for (let i = 0; i < HEIGHT; i += 1) { - for (let j = 0; j < WIDTH; j += 1) { - const p = i * WIDTH * 4 + j * 4; - if (isPixelFilled(data[i][j]) === false) { - ok = false; - } - // eslint-disable-next-line no-param-reassign, prefer-destructuring - buffer[p] = data[i][j][0]; - // eslint-disable-next-line no-param-reassign, prefer-destructuring - buffer[p + 1] = data[i][j][1]; - // eslint-disable-next-line no-param-reassign, prefer-destructuring - buffer[p + 2] = data[i][j][2]; - // eslint-disable-next-line no-param-reassign, prefer-destructuring - buffer[p + 3] = data[i][j][3]; - } - } - - if (!ok) { - const warningMessage = - 'You have invalid values for some pixels! Reseting them to default (0)'; - // eslint-disable-next-line no-console - console.warn(warningMessage); - errorLogger(warningMessage, false); - } -} - -/** @hidden */ -function readFromBuffer(pixelData: Uint8ClampedArray, src: Pixels) { - for (let i = 0; i < HEIGHT; i += 1) { - for (let j = 0; j < WIDTH; j += 1) { - const p = i * WIDTH * 4 + j * 4; - // eslint-disable-next-line no-param-reassign - src[i][j] = [ - pixelData[p], - pixelData[p + 1], - pixelData[p + 2], - pixelData[p + 3], - ]; - } - } -} - -/** @hidden */ -function drawImage(source: VideoElement | ImageElement): void { - canvasRenderingContext.drawImage(source, 0, 0, WIDTH, HEIGHT); - const pixelObj = canvasRenderingContext.getImageData(0, 0, WIDTH, HEIGHT); - readFromBuffer(pixelObj.data, pixels); - - // Runtime checks to guard against crashes - try { - filter(pixels, temporaryPixels); - writeToBuffer(pixelObj.data, temporaryPixels); - } catch (e: any) { - // eslint-disable-next-line no-console - console.error(JSON.stringify(e)); - const errMsg = `There is an error with filter function, filter will be reset to default. ${e.name}: ${e.message}`; - // eslint-disable-next-line no-console - console.error(errMsg); - - if (!e.name) { - errorLogger( - 'There is an error with filter function (error shown below). Filter will be reset back to the default. If you are facing an infinite loop error, you can consider increasing the timeout period (clock icon) at the top / reducing the frame dimensions.' - ); - - errorLogger([e], true); - } else { - errorLogger(errMsg, false); - } - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - filter = copy_image; - filter(pixels, temporaryPixels); - } - - canvasRenderingContext.putImageData(pixelObj, 0, 0); -} - -/** @hidden */ -function draw(timestamp: number): void { - // eslint-disable-next-line no-unused-vars - requestId = window.requestAnimationFrame(draw); - - if (prevTime === null) prevTime = timestamp; - - const elapsed = timestamp - prevTime; - if (elapsed > 1000 / FPS && videoIsPlaying) { - drawImage(videoElement); - prevTime = timestamp; - totalElapsedTime += elapsed; - if (toRunLateQueue) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - lateQueue(); - toRunLateQueue = false; - } - } -} - -/** @hidden */ -function playVideoElement() { - if (!videoIsPlaying) { - videoElement - .play() - .then(() => { - videoIsPlaying = true; - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.warn(err); - }); - } -} - -/** @hidden */ -function pauseVideoElement() { - if (videoIsPlaying) { - videoElement.pause(); - videoIsPlaying = false; - } -} - -/** @hidden */ -function startVideo(): void { - if (videoIsPlaying) return; - if (inputFeed === InputFeed.Camera) videoIsPlaying = true; - else playVideoElement(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - requestId = window.requestAnimationFrame(draw); -} - -/** - * Stops the loop that is drawing on image. - * - * @hidden - */ -function stopVideo(): void { - if (!videoIsPlaying) return; - if (inputFeed === InputFeed.Camera) videoIsPlaying = false; - else pauseVideoElement(); - window.cancelAnimationFrame(requestId); - prevTime = null; -} - -/** @hidden */ -function loadMedia(): void { - if (!navigator.mediaDevices.getUserMedia) { - const errMsg = 'The browser you are using does not support getUserMedia'; - // eslint-disable-next-line no-console - console.error(errMsg); - errorLogger(errMsg, false); - } - - // If video is already part of bundle state - if (videoElement.srcObject) return; - - navigator.mediaDevices - .getUserMedia({ video: true }) - .then((stream) => { - videoElement.srcObject = stream; - toRunLateQueue = true; - }) - .catch((error) => { - const errorMessage = `${error.name}: ${error.message}`; - // eslint-disable-next-line no-console - console.error(errorMessage); - errorLogger(errorMessage, false); - }); - - startVideo(); -} - -/** @hidden */ -function loadAlternative(): void { - try { - if (inputFeed === InputFeed.VideoURL) { - videoElement.src = url; - startVideo(); - } else if (inputFeed === InputFeed.ImageURL) { - imageElement.src = url; - } - } catch (e: any) { - // eslint-disable-next-line no-console - console.error(JSON.stringify(e)); - const errMsg = `There is an error loading the URL. ${e.name}: ${e.message}`; - // eslint-disable-next-line no-console - console.error(errMsg); - loadMedia(); - return; - } - toRunLateQueue = true; - videoElement.crossOrigin = 'anonymous'; - videoElement.loop = true; - imageElement.crossOrigin = 'anonymous'; - imageElement.onload = () => { - drawImage(imageElement); - }; -} - -/** - * Just draws once on image and stops video. - * - * @hidden - */ -function snapPicture(): void { - drawImage(imageElement); - stopVideo(); -} - -/** - * Update the FPS - * - * @hidden - */ -function updateFPS(fps: number): void { - if (fps < MIN_FPS || fps > MAX_FPS) return; - FPS = fps; -} - -/** - * Update the image dimensions. - * - * @hidden - */ -function updateDimensions(w: number, h: number): void { - // ignore if no change or bad inputs - if ( - (w === WIDTH && h === HEIGHT) || - w > MAX_WIDTH || - w < MIN_WIDTH || - h > MAX_HEIGHT || - h < MIN_HEIGHT - ) { - return; - } - - const status = videoIsPlaying; - stopVideo(); - - WIDTH = w; - HEIGHT = h; - - videoElement.width = w; - videoElement.height = h; - canvasElement.width = w; - canvasElement.height = h; - - setupData(); - - if (!status) { - setTimeout(() => snapPicture(), 50); - return; - } - - startVideo(); -} - -/** - * Updates the volume of the local video - * - * @hidden - */ -function updateVolume(v: number): void { - VOLUME = Math.max(0.0, Math.min(1.0, v)); - videoElement.volume = VOLUME; -} - -// queue is run when init is called -let queue: Queue = () => {}; - -/** - * Adds function to the queue - * - * @hidden - */ -function enqueue(funcToAdd: Queue): void { - const funcToRunFirst: Queue = queue; - queue = () => { - funcToRunFirst(); - funcToAdd(); - }; -} - -// lateQueue is run after media has properly loaded -let lateQueue: Queue = () => {}; - -/** - * Adds function to the lateQueue - * - * @hidden - */ -function lateEnqueue(funcToAdd: Queue): void { - const funcToRunFirst: Queue = lateQueue; - lateQueue = () => { - funcToRunFirst(); - funcToAdd(); - }; -} - -/** - * Used to initialise the video library. - * - * @returns a BundlePackage object containing Video's properties - * and other miscellaneous information relevant to tabs. - * @hidden - */ -function init( - image: ImageElement, - video: VideoElement, - canvas: CanvasElement, - _errorLogger: ErrorLogger, - _tabsPackage: TabsPacket -): BundlePacket { - imageElement = image; - videoElement = video; - canvasElement = canvas; - errorLogger = _errorLogger; - tabsPackage = _tabsPackage; - const context = canvasElement.getContext('2d'); - if (!context) throw new Error('Canvas context should not be null.'); - canvasRenderingContext = context; - setupData(); - if (inputFeed === InputFeed.Camera) { - loadMedia(); - } else { - loadAlternative(); - } - queue(); - return { HEIGHT, WIDTH, FPS, VOLUME, inputFeed }; -} - -/** - * Destructor that does necessary cleanup. - * - * @hidden - */ -function deinit(): void { - snapPicture(); - const stream = videoElement.srcObject; - if (!stream) { - return; - } - stream.getTracks().forEach((track) => { - track.stop(); - }); -} - -// ============================================================================= -// Module's Exposed Functions -// ============================================================================= - -/** - * Starts processing the image or video using the installed filter. - */ -export function start(): StartPacket { - return { - toReplString: () => '[Pix N Flix]', - init, - deinit, - startVideo, - snapPicture, - updateFPS, - updateVolume, - updateDimensions, - }; -} - -/** - * Returns the red component of the given pixel. - * - * @param pixel The given pixel - * @returns The red component as a number between 0 and 255 - */ -export function red_of(pixel: Pixel): number { - // returns the red value of pixel respectively - return pixel[0]; -} - -/** - * Returns the green component of the given pixel. - * - * @param pixel The given pixel - * @returns The green component as a number between 0 and 255 - */ -export function green_of(pixel: Pixel): number { - // returns the green value of pixel respectively - return pixel[1]; -} - -/** - * Returns the blue component of the given pixel. - * - * @param pixel The given pixel - * @returns The blue component as a number between 0 and 255 - */ -export function blue_of(pixel: Pixel): number { - // returns the blue value of pixel respectively - return pixel[2]; -} - -/** - * Returns the alpha component of the given pixel. - * - * @param pixel The given pixel - * @returns The alpha component as a number between 0 and 255 - */ -export function alpha_of(pixel: Pixel): number { - // returns the alpha value of pixel respectively - return pixel[3]; -} - -/** - * Assigns the given red, green, blue and alpha component values to - * the given pixel. - * - * @param pixel The given pixel - * @param r The red component as a number between 0 and 255 - * @param g The green component as a number between 0 and 255 - * @param b The blue component as a number between 0 and 255 - * @param a The alpha component as a number between 0 and 255 - */ -export function set_rgba( - pixel: Pixel, - r: number, - g: number, - b: number, - a: number -): void { - // assigns the r,g,b values to this pixel - // eslint-disable-next-line no-param-reassign - pixel[0] = r; - // eslint-disable-next-line no-param-reassign - pixel[1] = g; - // eslint-disable-next-line no-param-reassign - pixel[2] = b; - // eslint-disable-next-line no-param-reassign - pixel[3] = a; -} - -/** - * Returns the current height of the displayed images in - * pixels, i.e. the number of pixels in the vertical dimension. - * - * @returns The height of the displayed images (in pixels) - */ -export function image_height(): number { - return HEIGHT; -} - -/** - * Returns the current width of the displayed images in - * pixels, i.e. the number of pixels in the horizontal dimension. - * - * @returns The width of the displayed images (in pixels) - */ -export function image_width(): number { - return WIDTH; -} - -/** - * The default filter that just copies the source image to the - * destination image. - * - * @param src Source image - * @param dest Destination image - */ -export function copy_image(src: Pixels, dest: Pixels): void { - for (let i = 0; i < HEIGHT; i += 1) { - for (let j = 0; j < WIDTH; j += 1) { - // eslint-disable-next-line no-param-reassign - dest[i][j] = src[i][j]; - } - } -} - -/** - * Installs the given filter to be used to transform each source image from - * the live camera or from a local/remote file to a destination image that - * is then displayed on screen. - * - * A filter is a function that is applied to two - * two-dimensional arrays of Pixels: - * the source image and the destination image. - * - * @param filter The filter to be installed - */ -export function install_filter(_filter: Filter): void { - filter = _filter; -} - -/** - * Resets the installed filter to the default filter. - */ -export function reset_filter(): void { - install_filter(copy_image); -} - -/** - * Creates a black image. - * - * @hidden - */ -function new_image(): Pixels { - const img: Pixels = []; - for (let i = 0; i < HEIGHT; i += 1) { - img[i] = []; - for (let j = 0; j < WIDTH; j += 1) { - img[i][j] = [0, 0, 0, 255]; - } - } - return img; -} - -/** - * Returns a new filter that is equivalent to applying - * filter1 and then filter2. - * - * @param filter1 The first filter - * @param filter2 The second filter - * @returns The filter equivalent to applying filter1 and then filter2 - */ -export function compose_filter(filter1: Filter, filter2: Filter): Filter { - return (src, dest) => { - const temp = new_image(); - filter1(src, temp); - filter2(temp, dest); - }; -} - -/** - * Pauses the video at a set time after the video starts. - * - * @param pause_time Time in ms after the video starts. - */ -export function pause_at(pause_time: number): void { - // prevent negative pause_time - lateEnqueue(() => { - setTimeout( - tabsPackage.onClickStill, - pause_time >= 0 ? pause_time : -pause_time - ); - }); -} - -/** - * Sets the diemsions of the displayed images. - * Note: Only accepts width and height values within the range of 1 to 500. - * - * @param width The width of the displayed images (default value: 300) - * @param height The height of the displayed images (default value: 400) - */ -export function set_dimensions(width: number, height: number): void { - enqueue(() => updateDimensions(width, height)); -} - -/** - * Sets the framerate (i.e. frames per second (FPS)) of the video. - * Note: Only accepts FPS values within the range of 2 to 30. - * - * @param fps FPS of video (default value: 10) - */ -export function set_fps(fps: number): void { - enqueue(() => updateFPS(fps)); -} - -/** - * Sets the audio volume of the local video file played. - * Note: Only accepts volume value within the range of 0 to 100. - * - * @param volume Volume of video (Default value of 50) - */ -export function set_volume(volume: number): void { - enqueue(() => updateVolume(Math.max(0, Math.min(100, volume) / 100.0))); -} - -/** - * Sets pix_n_flix to use video or image feed from a local file - * instead of using the default live camera feed. - */ -export function use_local_file(): void { - inputFeed = InputFeed.Local; -} - -/** - * Sets pix_n_flix to use the image from the given URL as the image feed - * instead of using the default live camera feed. - * - * @param URL URL of the image - */ -export function use_image_url(URL: string) { - inputFeed = InputFeed.ImageURL; - url = URL; -} - -/** - * Sets pix_n_flix to use the video from the given URL as the video feed - * instead of using the default live camera feed. - * - * @param URL URL of the video - */ -export function use_video_url(URL: string) { - inputFeed = InputFeed.VideoURL; - url = URL; -} - -/** - * Returns the elapsed time in milliseconds since the start of the video. - * - * @returns The elapsed time in milliseconds since the start of the video - */ -export function get_video_time() { - return totalElapsedTime; -} +/** + * The pix_n_flix module allows us to process still images and videos. + * + * An Image (which is a still image or a frame of a video) is a + * two-dimensional array of Pixels, and a Pixel consists of red, blue and green color + * values, each ranging from 0 to 255. To access these color values of a Pixel, we + * provide the functions red_of, blue_of and green_of. + * + * A central element of pix_n_flix is the notion of a Filter, a function that is applied + * to two Images: the source Image and the destination Image. When a Filter is installed + * (using the function install_filter), it transforms each source Image from the live camera + * or from a local/remote file to a destination Image that is then displayed on screen + * in the Source Academy "Pix N Flix" tab (with a camera icon). + * + * The dimensions (i.e. width and height) of the displayed images can be set by the user using + * the function set_dimensions, and all source and destination Images of the Filters will + * also be set to the same dimensions. To access the current dimensions of the Images, the user + * can use the functions image_width and image_height. + * + * @module pix_n_flix + */ + +/* eslint-disable @typescript-eslint/no-shadow */ +import { + CanvasElement, + VideoElement, + ErrorLogger, + StartPacket, + Pixel, + Pixels, + Filter, + Queue, + TabsPacket, + BundlePacket, + InputFeed, + ImageElement, +} from './types'; + +import { + DEFAULT_WIDTH, + DEFAULT_HEIGHT, + DEFAULT_FPS, + DEFAULT_VOLUME, + MAX_HEIGHT, + MIN_HEIGHT, + MAX_WIDTH, + MIN_WIDTH, + MAX_FPS, + MIN_FPS, +} from './constants'; + +// Global Variables +let WIDTH: number = DEFAULT_WIDTH; +let HEIGHT: number = DEFAULT_HEIGHT; +let FPS: number = DEFAULT_FPS; +let VOLUME: number = DEFAULT_VOLUME; + +let imageElement: ImageElement; +let videoElement: VideoElement; +let canvasElement: CanvasElement; +let canvasRenderingContext: CanvasRenderingContext2D; +let errorLogger: ErrorLogger; +let tabsPackage: TabsPacket; + +const pixels: Pixels = []; +const temporaryPixels: Pixels = []; +// eslint-disable-next-line @typescript-eslint/no-use-before-define +let filter: Filter = copy_image; + +let toRunLateQueue: boolean = false; +let videoIsPlaying: boolean = false; + +let requestId: number; +let prevTime: number | null = null; +let totalElapsedTime: number = 0; + +let inputFeed: InputFeed = InputFeed.Camera; +let url: string = ''; + +// ============================================================================= +// Module's Private Functions +// ============================================================================= + +/** @hidden */ +function setupData(): void { + for (let i = 0; i < HEIGHT; i += 1) { + pixels[i] = []; + temporaryPixels[i] = []; + for (let j = 0; j < WIDTH; j += 1) { + pixels[i][j] = [0, 0, 0, 255]; + temporaryPixels[i][j] = [0, 0, 0, 255]; + } + } +} + +/** @hidden */ +function isPixelFilled(pixel: Pixel): boolean { + let ok = true; + for (let i = 0; i < 4; i += 1) { + if (pixel[i] >= 0 && pixel[i] <= 255) { + // eslint-disable-next-line no-continue + continue; + } + ok = false; + // eslint-disable-next-line no-param-reassign + pixel[i] = 0; + } + return ok; +} + +/** @hidden */ +function writeToBuffer(buffer: Uint8ClampedArray, data: Pixels) { + let ok: boolean = true; + + for (let i = 0; i < HEIGHT; i += 1) { + for (let j = 0; j < WIDTH; j += 1) { + const p = i * WIDTH * 4 + j * 4; + if (isPixelFilled(data[i][j]) === false) { + ok = false; + } + // eslint-disable-next-line no-param-reassign, prefer-destructuring + buffer[p] = data[i][j][0]; + // eslint-disable-next-line no-param-reassign, prefer-destructuring + buffer[p + 1] = data[i][j][1]; + // eslint-disable-next-line no-param-reassign, prefer-destructuring + buffer[p + 2] = data[i][j][2]; + // eslint-disable-next-line no-param-reassign, prefer-destructuring + buffer[p + 3] = data[i][j][3]; + } + } + + if (!ok) { + const warningMessage + = 'You have invalid values for some pixels! Reseting them to default (0)'; + // eslint-disable-next-line no-console + console.warn(warningMessage); + errorLogger(warningMessage, false); + } +} + +/** @hidden */ +function readFromBuffer(pixelData: Uint8ClampedArray, src: Pixels) { + for (let i = 0; i < HEIGHT; i += 1) { + for (let j = 0; j < WIDTH; j += 1) { + const p = i * WIDTH * 4 + j * 4; + // eslint-disable-next-line no-param-reassign + src[i][j] = [ + pixelData[p], + pixelData[p + 1], + pixelData[p + 2], + pixelData[p + 3], + ]; + } + } +} + +/** @hidden */ +function drawImage(source: VideoElement | ImageElement): void { + canvasRenderingContext.drawImage(source, 0, 0, WIDTH, HEIGHT); + const pixelObj = canvasRenderingContext.getImageData(0, 0, WIDTH, HEIGHT); + readFromBuffer(pixelObj.data, pixels); + + // Runtime checks to guard against crashes + try { + filter(pixels, temporaryPixels); + writeToBuffer(pixelObj.data, temporaryPixels); + } catch (e: any) { + // eslint-disable-next-line no-console + console.error(JSON.stringify(e)); + const errMsg = `There is an error with filter function, filter will be reset to default. ${e.name}: ${e.message}`; + // eslint-disable-next-line no-console + console.error(errMsg); + + if (!e.name) { + errorLogger( + 'There is an error with filter function (error shown below). Filter will be reset back to the default. If you are facing an infinite loop error, you can consider increasing the timeout period (clock icon) at the top / reducing the frame dimensions.', + ); + + errorLogger([e], true); + } else { + errorLogger(errMsg, false); + } + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + filter = copy_image; + filter(pixels, temporaryPixels); + } + + canvasRenderingContext.putImageData(pixelObj, 0, 0); +} + +/** @hidden */ +function draw(timestamp: number): void { + // eslint-disable-next-line no-unused-vars + requestId = window.requestAnimationFrame(draw); + + if (prevTime === null) prevTime = timestamp; + + const elapsed = timestamp - prevTime; + if (elapsed > 1000 / FPS && videoIsPlaying) { + drawImage(videoElement); + prevTime = timestamp; + totalElapsedTime += elapsed; + if (toRunLateQueue) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + lateQueue(); + toRunLateQueue = false; + } + } +} + +/** @hidden */ +function playVideoElement() { + if (!videoIsPlaying) { + videoElement + .play() + .then(() => { + videoIsPlaying = true; + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.warn(err); + }); + } +} + +/** @hidden */ +function pauseVideoElement() { + if (videoIsPlaying) { + videoElement.pause(); + videoIsPlaying = false; + } +} + +/** @hidden */ +function startVideo(): void { + if (videoIsPlaying) return; + if (inputFeed === InputFeed.Camera) videoIsPlaying = true; + else playVideoElement(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + requestId = window.requestAnimationFrame(draw); +} + +/** + * Stops the loop that is drawing on image. + * + * @hidden + */ +function stopVideo(): void { + if (!videoIsPlaying) return; + if (inputFeed === InputFeed.Camera) videoIsPlaying = false; + else pauseVideoElement(); + window.cancelAnimationFrame(requestId); + prevTime = null; +} + +/** @hidden */ +function loadMedia(): void { + if (!navigator.mediaDevices.getUserMedia) { + const errMsg = 'The browser you are using does not support getUserMedia'; + // eslint-disable-next-line no-console + console.error(errMsg); + errorLogger(errMsg, false); + } + + // If video is already part of bundle state + if (videoElement.srcObject) return; + + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + videoElement.srcObject = stream; + toRunLateQueue = true; + }) + .catch((error) => { + const errorMessage = `${error.name}: ${error.message}`; + // eslint-disable-next-line no-console + console.error(errorMessage); + errorLogger(errorMessage, false); + }); + + startVideo(); +} + +/** @hidden */ +function loadAlternative(): void { + try { + if (inputFeed === InputFeed.VideoURL) { + videoElement.src = url; + startVideo(); + } else if (inputFeed === InputFeed.ImageURL) { + imageElement.src = url; + } + } catch (e: any) { + // eslint-disable-next-line no-console + console.error(JSON.stringify(e)); + const errMsg = `There is an error loading the URL. ${e.name}: ${e.message}`; + // eslint-disable-next-line no-console + console.error(errMsg); + loadMedia(); + return; + } + toRunLateQueue = true; + videoElement.crossOrigin = 'anonymous'; + videoElement.loop = true; + imageElement.crossOrigin = 'anonymous'; + imageElement.onload = () => { + drawImage(imageElement); + }; +} + +/** + * Just draws once on image and stops video. + * + * @hidden + */ +function snapPicture(): void { + drawImage(imageElement); + stopVideo(); +} + +/** + * Update the FPS + * + * @hidden + */ +function updateFPS(fps: number): void { + if (fps < MIN_FPS || fps > MAX_FPS) return; + FPS = fps; +} + +/** + * Update the image dimensions. + * + * @hidden + */ +function updateDimensions(w: number, h: number): void { + // ignore if no change or bad inputs + if ( + (w === WIDTH && h === HEIGHT) + || w > MAX_WIDTH + || w < MIN_WIDTH + || h > MAX_HEIGHT + || h < MIN_HEIGHT + ) { + return; + } + + const status = videoIsPlaying; + stopVideo(); + + WIDTH = w; + HEIGHT = h; + + videoElement.width = w; + videoElement.height = h; + canvasElement.width = w; + canvasElement.height = h; + + setupData(); + + if (!status) { + setTimeout(() => snapPicture(), 50); + return; + } + + startVideo(); +} + +/** + * Updates the volume of the local video + * + * @hidden + */ +function updateVolume(v: number): void { + VOLUME = Math.max(0.0, Math.min(1.0, v)); + videoElement.volume = VOLUME; +} + +// queue is run when init is called +let queue: Queue = () => {}; + +/** + * Adds function to the queue + * + * @hidden + */ +function enqueue(funcToAdd: Queue): void { + const funcToRunFirst: Queue = queue; + queue = () => { + funcToRunFirst(); + funcToAdd(); + }; +} + +// lateQueue is run after media has properly loaded +let lateQueue: Queue = () => {}; + +/** + * Adds function to the lateQueue + * + * @hidden + */ +function lateEnqueue(funcToAdd: Queue): void { + const funcToRunFirst: Queue = lateQueue; + lateQueue = () => { + funcToRunFirst(); + funcToAdd(); + }; +} + +/** + * Used to initialise the video library. + * + * @returns a BundlePackage object containing Video's properties + * and other miscellaneous information relevant to tabs. + * @hidden + */ +function init( + image: ImageElement, + video: VideoElement, + canvas: CanvasElement, + _errorLogger: ErrorLogger, + _tabsPackage: TabsPacket, +): BundlePacket { + imageElement = image; + videoElement = video; + canvasElement = canvas; + errorLogger = _errorLogger; + tabsPackage = _tabsPackage; + const context = canvasElement.getContext('2d'); + if (!context) throw new Error('Canvas context should not be null.'); + canvasRenderingContext = context; + setupData(); + if (inputFeed === InputFeed.Camera) { + loadMedia(); + } else { + loadAlternative(); + } + queue(); + return { + HEIGHT, + WIDTH, + FPS, + VOLUME, + inputFeed, + }; +} + +/** + * Destructor that does necessary cleanup. + * + * @hidden + */ +function deinit(): void { + snapPicture(); + const stream = videoElement.srcObject; + if (!stream) { + return; + } + stream.getTracks() + .forEach((track) => { + track.stop(); + }); +} + +// ============================================================================= +// Module's Exposed Functions +// ============================================================================= + +/** + * Starts processing the image or video using the installed filter. + */ +export function start(): StartPacket { + return { + toReplString: () => '[Pix N Flix]', + init, + deinit, + startVideo, + snapPicture, + updateFPS, + updateVolume, + updateDimensions, + }; +} + +/** + * Returns the red component of the given pixel. + * + * @param pixel The given pixel + * @returns The red component as a number between 0 and 255 + */ +export function red_of(pixel: Pixel): number { + // returns the red value of pixel respectively + return pixel[0]; +} + +/** + * Returns the green component of the given pixel. + * + * @param pixel The given pixel + * @returns The green component as a number between 0 and 255 + */ +export function green_of(pixel: Pixel): number { + // returns the green value of pixel respectively + return pixel[1]; +} + +/** + * Returns the blue component of the given pixel. + * + * @param pixel The given pixel + * @returns The blue component as a number between 0 and 255 + */ +export function blue_of(pixel: Pixel): number { + // returns the blue value of pixel respectively + return pixel[2]; +} + +/** + * Returns the alpha component of the given pixel. + * + * @param pixel The given pixel + * @returns The alpha component as a number between 0 and 255 + */ +export function alpha_of(pixel: Pixel): number { + // returns the alpha value of pixel respectively + return pixel[3]; +} + +/** + * Assigns the given red, green, blue and alpha component values to + * the given pixel. + * + * @param pixel The given pixel + * @param r The red component as a number between 0 and 255 + * @param g The green component as a number between 0 and 255 + * @param b The blue component as a number between 0 and 255 + * @param a The alpha component as a number between 0 and 255 + */ +export function set_rgba( + pixel: Pixel, + r: number, + g: number, + b: number, + a: number, +): void { + // assigns the r,g,b values to this pixel + // eslint-disable-next-line no-param-reassign + pixel[0] = r; + // eslint-disable-next-line no-param-reassign + pixel[1] = g; + // eslint-disable-next-line no-param-reassign + pixel[2] = b; + // eslint-disable-next-line no-param-reassign + pixel[3] = a; +} + +/** + * Returns the current height of the displayed images in + * pixels, i.e. the number of pixels in the vertical dimension. + * + * @returns The height of the displayed images (in pixels) + */ +export function image_height(): number { + return HEIGHT; +} + +/** + * Returns the current width of the displayed images in + * pixels, i.e. the number of pixels in the horizontal dimension. + * + * @returns The width of the displayed images (in pixels) + */ +export function image_width(): number { + return WIDTH; +} + +/** + * The default filter that just copies the source image to the + * destination image. + * + * @param src Source image + * @param dest Destination image + */ +export function copy_image(src: Pixels, dest: Pixels): void { + for (let i = 0; i < HEIGHT; i += 1) { + for (let j = 0; j < WIDTH; j += 1) { + // eslint-disable-next-line no-param-reassign + dest[i][j] = src[i][j]; + } + } +} + +/** + * Installs the given filter to be used to transform each source image from + * the live camera or from a local/remote file to a destination image that + * is then displayed on screen. + * + * A filter is a function that is applied to two + * two-dimensional arrays of Pixels: + * the source image and the destination image. + * + * @param filter The filter to be installed + */ +export function install_filter(_filter: Filter): void { + filter = _filter; +} + +/** + * Resets the installed filter to the default filter. + */ +export function reset_filter(): void { + install_filter(copy_image); +} + +/** + * Creates a black image. + * + * @hidden + */ +function new_image(): Pixels { + const img: Pixels = []; + for (let i = 0; i < HEIGHT; i += 1) { + img[i] = []; + for (let j = 0; j < WIDTH; j += 1) { + img[i][j] = [0, 0, 0, 255]; + } + } + return img; +} + +/** + * Returns a new filter that is equivalent to applying + * filter1 and then filter2. + * + * @param filter1 The first filter + * @param filter2 The second filter + * @returns The filter equivalent to applying filter1 and then filter2 + */ +export function compose_filter(filter1: Filter, filter2: Filter): Filter { + return (src, dest) => { + const temp = new_image(); + filter1(src, temp); + filter2(temp, dest); + }; +} + +/** + * Pauses the video at a set time after the video starts. + * + * @param pause_time Time in ms after the video starts. + */ +export function pause_at(pause_time: number): void { + // prevent negative pause_time + lateEnqueue(() => { + setTimeout( + tabsPackage.onClickStill, + pause_time >= 0 ? pause_time : -pause_time, + ); + }); +} + +/** + * Sets the diemsions of the displayed images. + * Note: Only accepts width and height values within the range of 1 to 500. + * + * @param width The width of the displayed images (default value: 300) + * @param height The height of the displayed images (default value: 400) + */ +export function set_dimensions(width: number, height: number): void { + enqueue(() => updateDimensions(width, height)); +} + +/** + * Sets the framerate (i.e. frames per second (FPS)) of the video. + * Note: Only accepts FPS values within the range of 2 to 30. + * + * @param fps FPS of video (default value: 10) + */ +export function set_fps(fps: number): void { + enqueue(() => updateFPS(fps)); +} + +/** + * Sets the audio volume of the local video file played. + * Note: Only accepts volume value within the range of 0 to 100. + * + * @param volume Volume of video (Default value of 50) + */ +export function set_volume(volume: number): void { + enqueue(() => updateVolume(Math.max(0, Math.min(100, volume) / 100.0))); +} + +/** + * Sets pix_n_flix to use video or image feed from a local file + * instead of using the default live camera feed. + */ +export function use_local_file(): void { + inputFeed = InputFeed.Local; +} + +/** + * Sets pix_n_flix to use the image from the given URL as the image feed + * instead of using the default live camera feed. + * + * @param URL URL of the image + */ +export function use_image_url(URL: string) { + inputFeed = InputFeed.ImageURL; + url = URL; +} + +/** + * Sets pix_n_flix to use the video from the given URL as the video feed + * instead of using the default live camera feed. + * + * @param URL URL of the video + */ +export function use_video_url(URL: string) { + inputFeed = InputFeed.VideoURL; + url = URL; +} + +/** + * Returns the elapsed time in milliseconds since the start of the video. + * + * @returns The elapsed time in milliseconds since the start of the video + */ +export function get_video_time() { + return totalElapsedTime; +} diff --git a/src/bundles/rune/functions.ts b/src/bundles/rune/functions.ts index c82d368f4..b1ba66a10 100644 --- a/src/bundles/rune/functions.ts +++ b/src/bundles/rune/functions.ts @@ -1,1024 +1,1024 @@ -/* eslint-disable max-classes-per-file */ -/** - * The module `rune` provides functions for drawing runes. - * - * A *Rune* is defined by its vertices (x,y,z,t), the colors on its vertices (r,g,b,a), a transformation matrix for rendering the Rune and a (could be empty) list of its sub-Runes. - * @module rune - */ -import { mat4, vec3 } from 'gl-matrix'; -import { - Rune, - NormalRune, - RuneAnimation, - DrawnRune, - drawRunesToFrameBuffer, - AnimatedRune, -} from './rune'; -import { - getSquare, - getBlank, - getRcross, - getSail, - getTriangle, - getCorner, - getNova, - getCircle, - getHeart, - getPentagram, - getRibbon, - throwIfNotRune, - addColorFromHex, - colorPalette, - hexToColor, -} from './runes_ops'; -import { - FrameBufferWithTexture, - getWebGlFromCanvas, - initFramebufferObject, - initShaderProgram, -} from './runes_webgl'; - -/** @hidden */ -export const drawnRunes: (DrawnRune | AnimatedRune)[] = []; - -// ============================================================================= -// Basic Runes -// ============================================================================= - -/** - * Rune with the shape of a full square - * - * @category Primitive - */ -export const square: Rune = getSquare(); -/** - * Rune with the shape of a blank square - * - * @category Primitive - */ -export const blank: Rune = getBlank(); -/** - * Rune with the shape of a - * small square inside a large square, - * each diagonally split into a - * black and white half - * - * @category Primitive - */ -export const rcross: Rune = getRcross(); -/** - * Rune with the shape of a sail - * - * @category Primitive - */ -export const sail: Rune = getSail(); -/** - * Rune with the shape of a triangle - * - * @category Primitive - */ -export const triangle: Rune = getTriangle(); -/** - * Rune with black triangle, - * filling upper right corner - * - * @category Primitive - */ -export const corner: Rune = getCorner(); -/** - * Rune with the shape of two overlapping - * triangles, residing in the upper half - * of the shape - * - * @category Primitive - */ -export const nova: Rune = getNova(); -/** - * Rune with the shape of a circle - * - * @category Primitive - */ -export const circle: Rune = getCircle(); -/** - * Rune with the shape of a heart - * - * @category Primitive - */ -export const heart: Rune = getHeart(); -/** - * Rune with the shape of a pentagram - * - * @category Primitive - */ -export const pentagram: Rune = getPentagram(); -/** - * Rune with the shape of a ribbon - * winding outwards in an anticlockwise spiral - * - * @category Primitive - */ -export const ribbon: Rune = getRibbon(); - -// ============================================================================= -// Textured Runes -// ============================================================================= -/** - * Create a rune using the image provided in the url - * @param {string} imageUrl URL to the image that is used to create the rune. - * Note that the url must be from a domain that allows CORS. - * @returns {Rune} Rune created using the image. - * - * @category Main - */ -export function from_url(imageUrl: string): Rune { - const rune = getSquare(); - rune.texture = new Image(); - rune.texture.crossOrigin = 'anonymous'; - rune.texture.src = imageUrl; - return rune; -} - -// ============================================================================= -// XY-axis Transformation functions -// ============================================================================= - -/** - * Scales a given Rune by separate factors in x and y direction - * @param {number} ratio_x - Scaling factor in x direction - * @param {number} ratio_y - Scaling factor in y direction - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting scaled Rune - * - * @category Main - */ -export function scale_independent( - ratio_x: number, - ratio_y: number, - rune: Rune -): Rune { - throwIfNotRune('scale_independent', rune); - const scaleVec = vec3.fromValues(ratio_x, ratio_y, 1); - const scaleMat = mat4.create(); - mat4.scale(scaleMat, scaleMat, scaleVec); - - const wrapperMat = mat4.create(); - mat4.multiply(wrapperMat, scaleMat, wrapperMat); - return Rune.of({ - subRunes: [rune], - transformMatrix: wrapperMat, - }); -} - -/** - * Scales a given Rune by a given factor in both x and y direction - * @param {number} ratio - Scaling factor - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting scaled Rune - * - * @category Main - */ -export function scale(ratio: number, rune: Rune): Rune { - throwIfNotRune('scale', rune); - return scale_independent(ratio, ratio, rune); -} - -/** - * Translates a given Rune by given values in x and y direction - * @param {number} x - Translation in x direction - * @param {number} y - Translation in y direction - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting translated Rune - * - * @category Main - */ -export function translate(x: number, y: number, rune: Rune): Rune { - throwIfNotRune('translate', rune); - const translateVec = vec3.fromValues(x, -y, 0); - const translateMat = mat4.create(); - mat4.translate(translateMat, translateMat, translateVec); - - const wrapperMat = mat4.create(); - mat4.multiply(wrapperMat, translateMat, wrapperMat); - return Rune.of({ - subRunes: [rune], - transformMatrix: wrapperMat, - }); -} - -/** - * Rotates a given Rune by a given angle, - * given in radians, in anti-clockwise direction. - * Note that parts of the Rune - * may be cropped as a result. - * @param {number} rad - Angle in radians - * @param {Rune} rune - Given Rune - * @return {Rune} Rotated Rune - * - * @category Main - */ -export function rotate(rad: number, rune: Rune): Rune { - throwIfNotRune('rotate', rune); - const rotateMat = mat4.create(); - mat4.rotateZ(rotateMat, rotateMat, rad); - - const wrapperMat = mat4.create(); - mat4.multiply(wrapperMat, rotateMat, wrapperMat); - return Rune.of({ - subRunes: [rune], - transformMatrix: wrapperMat, - }); -} - -/** - * Makes a new Rune from two given Runes by - * placing the first on top of the second - * such that the first one occupies frac - * portion of the height of the result and - * the second the rest - * @param {number} frac - Fraction between 0 and 1 (inclusive) - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function stack_frac(frac: number, rune1: Rune, rune2: Rune): Rune { - throwIfNotRune('stack_frac', rune1); - throwIfNotRune('stack_frac', rune2); - - if (!(frac >= 0 && frac <= 1)) { - throw Error('stack_frac can only take fraction in [0,1].'); - } - - const upper = translate(0, -(1 - frac), scale_independent(1, frac, rune1)); - const lower = translate(0, frac, scale_independent(1, 1 - frac, rune2)); - return Rune.of({ - subRunes: [upper, lower], - }); -} - -/** - * Makes a new Rune from two given Runes by - * placing the first on top of the second, each - * occupying equal parts of the height of the - * result - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function stack(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune('stack', rune1, rune2); - return stack_frac(1 / 2, rune1, rune2); -} - -/** - * Makes a new Rune from a given Rune - * by vertically stacking n copies of it - * @param {number} n - Positive integer - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function stackn(n: number, rune: Rune): Rune { - throwIfNotRune('stackn', rune); - if (n === 1) { - return rune; - } - return stack_frac(1 / n, rune, stackn(n - 1, rune)); -} - -/** - * Makes a new Rune from a given Rune - * by turning it a quarter-turn around the centre in - * clockwise direction. - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function quarter_turn_right(rune: Rune): Rune { - throwIfNotRune('quarter_turn_right', rune); - return rotate(-Math.PI / 2, rune); -} - -/** - * Makes a new Rune from a given Rune - * by turning it a quarter-turn in - * anti-clockwise direction. - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function quarter_turn_left(rune: Rune): Rune { - throwIfNotRune('quarter_turn_left', rune); - return rotate(Math.PI / 2, rune); -} - -/** - * Makes a new Rune from a given Rune - * by turning it upside-down - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function turn_upside_down(rune: Rune): Rune { - throwIfNotRune('turn_upside_down', rune); - return rotate(Math.PI, rune); -} - -/** - * Makes a new Rune from two given Runes by - * placing the first on the left of the second - * such that the first one occupies frac - * portion of the width of the result and - * the second the rest - * @param {number} frac - Fraction between 0 and 1 (inclusive) - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function beside_frac(frac: number, rune1: Rune, rune2: Rune): Rune { - throwIfNotRune('beside_frac', rune1, rune2); - - if (!(frac >= 0 && frac <= 1)) { - throw Error('beside_frac can only take fraction in [0,1].'); - } - - const left = translate(-(1 - frac), 0, scale_independent(frac, 1, rune1)); - const right = translate(frac, 0, scale_independent(1 - frac, 1, rune2)); - return Rune.of({ - subRunes: [left, right], - }); -} - -/** - * Makes a new Rune from two given Runes by - * placing the first on the left of the second, - * both occupying equal portions of the width - * of the result - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function beside(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune('beside', rune1, rune2); - return beside_frac(1 / 2, rune1, rune2); -} - -/** - * Makes a new Rune from a given Rune by - * flipping it around a horizontal axis, - * turning it upside down - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function flip_vert(rune: Rune): Rune { - throwIfNotRune('flip_vert', rune); - return scale_independent(1, -1, rune); -} - -/** - * Makes a new Rune from a given Rune by - * flipping it around a vertical axis, - * creating a mirror image - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function flip_horiz(rune: Rune): Rune { - throwIfNotRune('flip_horiz', rune); - return scale_independent(-1, 1, rune); -} - -/** - * Makes a new Rune from a given Rune by - * arranging into a square for copies of the - * given Rune in different orientations - * @param {Rune} rune - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function make_cross(rune: Rune): Rune { - throwIfNotRune('make_cross', rune); - return stack( - beside(quarter_turn_right(rune), rotate(Math.PI, rune)), - beside(rune, rotate(Math.PI / 2, rune)) - ); -} - -/** - * Applies a given function n times to an initial value - * @param {number} n - A non-negative integer - * @param {function} pattern - Unary function from Rune to Rune - * @param {Rune} initial - The initial Rune - * @return {Rune} - Result of n times application of pattern to initial: - * pattern(pattern(...pattern(pattern(initial))...)) - * - * @category Main - */ -export function repeat_pattern( - n: number, - pattern: (a: Rune) => Rune, - initial: Rune -): Rune { - if (n === 0) { - return initial; - } - return pattern(repeat_pattern(n - 1, pattern, initial)); -} - -// ============================================================================= -// Z-axis Transformation functions -// ============================================================================= - -/** - * The depth range of the z-axis of a rune is [0,-1], this function gives a [0, -frac] of the depth range to rune1 and the rest to rune2. - * @param {number} frac - Fraction between 0 and 1 (inclusive) - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Rune - * - * @category Main - */ -export function overlay_frac(frac: number, rune1: Rune, rune2: Rune): Rune { - // to developer: please read https://www.tutorialspoint.com/webgl/webgl_basics.htm to understand the webgl z-axis interpretation. - // The key point is that positive z is closer to the screen. Hence, the image at the back should have smaller z value. Primitive runes have z = 0. - throwIfNotRune('overlay_frac', rune1); - throwIfNotRune('overlay_frac', rune2); - if (!(frac >= 0 && frac <= 1)) { - throw Error('overlay_frac can only take fraction in [0,1].'); - } - // by definition, when frac == 0 or 1, the back rune will overlap with the front rune. - // however, this would cause graphical glitch because overlapping is physically impossible - // we hack this problem by clipping the frac input from [0,1] to [1E-6, 1-1E-6] - // this should not be graphically noticable - let useFrac = frac; - const minFrac = 0.000001; - const maxFrac = 1 - minFrac; - if (useFrac < minFrac) { - useFrac = minFrac; - } - if (useFrac > maxFrac) { - useFrac = maxFrac; - } - - const frontMat = mat4.create(); - // z: scale by frac - mat4.scale(frontMat, frontMat, vec3.fromValues(1, 1, useFrac)); - const front = Rune.of({ - subRunes: [rune1], - transformMatrix: frontMat, - }); - - const backMat = mat4.create(); - // need to apply transformation in backwards order! - mat4.translate(backMat, backMat, vec3.fromValues(0, 0, -useFrac)); - mat4.scale(backMat, backMat, vec3.fromValues(1, 1, 1 - useFrac)); - const back = Rune.of({ - subRunes: [rune2], - transformMatrix: backMat, - }); - - return Rune.of({ - subRunes: [front, back], // render front first to avoid redrawing - }); -} - -/** - * The depth range of the z-axis of a rune is [0,-1], this function maps the depth range of rune1 and rune2 to [0,-0.5] and [-0.5,-1] respectively. - * @param {Rune} rune1 - Given Rune - * @param {Rune} rune2 - Given Rune - * @return {Rune} Resulting Runes - * - * @category Main - */ -export function overlay(rune1: Rune, rune2: Rune): Rune { - throwIfNotRune('overlay', rune1); - throwIfNotRune('overlay', rune2); - return overlay_frac(0.5, rune1, rune2); -} - -// ============================================================================= -// Color functions -// ============================================================================= - -/** - * Adds color to rune by specifying - * the red, green, blue (RGB) value, ranging from 0.0 to 1.0. - * RGB is additive: if all values are 1, the color is white, - * and if all values are 0, the color is black. - * @param {Rune} rune - The rune to add color to - * @param {number} r - Red value [0.0-1.0] - * @param {number} g - Green value [0.0-1.0] - * @param {number} b - Blue value [0.0-1.0] - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function color(rune: Rune, r: number, g: number, b: number): Rune { - throwIfNotRune('color', rune); - - const colorVector = [r, g, b, 1]; - return Rune.of({ - colors: new Float32Array(colorVector), - subRunes: [rune], - }); -} - -/** - * Gives random color to the given rune. - * The color is chosen randomly from the following nine - * colors: red, pink, purple, indigo, blue, green, yellow, orange, brown - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function random_color(rune: Rune): Rune { - throwIfNotRune('random_color', rune); - const randomColor = hexToColor( - colorPalette[Math.floor(Math.random() * colorPalette.length)] - ); - - return Rune.of({ - colors: new Float32Array(randomColor), - subRunes: [rune], - }); -} - -/** - * Colors the given rune red (#F44336). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function red(rune: Rune): Rune { - throwIfNotRune('red', rune); - return addColorFromHex(rune, '#F44336'); -} - -/** - * Colors the given rune pink (#E91E63s). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function pink(rune: Rune): Rune { - throwIfNotRune('pink', rune); - return addColorFromHex(rune, '#E91E63'); -} - -/** - * Colors the given rune purple (#AA00FF). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function purple(rune: Rune): Rune { - throwIfNotRune('purple', rune); - return addColorFromHex(rune, '#AA00FF'); -} - -/** - * Colors the given rune indigo (#3F51B5). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function indigo(rune: Rune): Rune { - throwIfNotRune('indigo', rune); - return addColorFromHex(rune, '#3F51B5'); -} - -/** - * Colors the given rune blue (#2196F3). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function blue(rune: Rune): Rune { - throwIfNotRune('blue', rune); - return addColorFromHex(rune, '#2196F3'); -} - -/** - * Colors the given rune green (#4CAF50). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function green(rune: Rune): Rune { - throwIfNotRune('green', rune); - return addColorFromHex(rune, '#4CAF50'); -} - -/** - * Colors the given rune yellow (#FFEB3B). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function yellow(rune: Rune): Rune { - throwIfNotRune('yellow', rune); - return addColorFromHex(rune, '#FFEB3B'); -} - -/** - * Colors the given rune orange (#FF9800). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function orange(rune: Rune): Rune { - throwIfNotRune('orange', rune); - return addColorFromHex(rune, '#FF9800'); -} - -/** - * Colors the given rune brown. - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function brown(rune: Rune): Rune { - throwIfNotRune('brown', rune); - return addColorFromHex(rune, '#795548'); -} - -/** - * Colors the given rune black (#000000). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function black(rune: Rune): Rune { - throwIfNotRune('black', rune); - return addColorFromHex(rune, '#000000'); -} - -/** - * Colors the given rune white (#FFFFFF). - * @param {Rune} rune - The rune to color - * @returns {Rune} The colored Rune - * - * @category Color - */ -export function white(rune: Rune): Rune { - throwIfNotRune('white', rune); - return addColorFromHex(rune, '#FFFFFF'); -} - -// ============================================================================= -// Drawing functions -// ============================================================================= - -/** - * Renders the specified Rune in a tab as a basic drawing. - * @param rune - The Rune to render - * @return {Rune} The specified Rune - * - * @category Main - */ -export function show(rune: Rune): Rune { - throwIfNotRune('show', rune); - drawnRunes.push(new NormalRune(rune)); - return rune; -} - -/** @hidden */ -export class AnaglyphRune extends DrawnRune { - private static readonly anaglyphVertexShader = ` - precision mediump float; - attribute vec4 a_position; - varying highp vec2 v_texturePosition; - void main() { - gl_Position = a_position; - // texture position is in [0,1], vertex position is in [-1,1] - v_texturePosition.x = (a_position.x + 1.0) / 2.0; - v_texturePosition.y = (a_position.y + 1.0) / 2.0; - } - `; - - private static readonly anaglyphFragmentShader = ` - precision mediump float; - uniform sampler2D u_sampler_red; - uniform sampler2D u_sampler_cyan; - varying highp vec2 v_texturePosition; - void main() { - gl_FragColor = texture2D(u_sampler_red, v_texturePosition) - + texture2D(u_sampler_cyan, v_texturePosition) - 1.0; - gl_FragColor.a = 1.0; - } - `; - - constructor(rune: Rune) { - super(rune, false); - } - - public draw = (canvas: HTMLCanvasElement) => { - const gl = getWebGlFromCanvas(canvas); - - // before draw the runes to framebuffer, we need to first draw a white background to cover the transparent places - const runes = white(overlay_frac(0.999999999, blank, scale(2.2, square))) - .flatten() - .concat(this.rune.flatten()); - - // calculate the left and right camera matrices - const halfEyeDistance = 0.03; - const leftCameraMatrix = mat4.create(); - mat4.lookAt( - leftCameraMatrix, - vec3.fromValues(-halfEyeDistance, 0, 0), - vec3.fromValues(0, 0, -0.4), - vec3.fromValues(0, 1, 0) - ); - const rightCameraMatrix = mat4.create(); - mat4.lookAt( - rightCameraMatrix, - vec3.fromValues(halfEyeDistance, 0, 0), - vec3.fromValues(0, 0, -0.4), - vec3.fromValues(0, 1, 0) - ); - - // left/right eye images are drawn into respective framebuffers - const leftBuffer = initFramebufferObject(gl); - const rightBuffer = initFramebufferObject(gl); - drawRunesToFrameBuffer( - gl, - runes, - leftCameraMatrix, - new Float32Array([1, 0, 0, 1]), - leftBuffer.framebuffer, - true - ); - drawRunesToFrameBuffer( - gl, - runes, - rightCameraMatrix, - new Float32Array([0, 1, 1, 1]), - rightBuffer.framebuffer, - true - ); - - // prepare to draw to screen by setting framebuffer to null - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - // prepare the shader program to combine the left/right eye images - const shaderProgram = initShaderProgram( - gl, - AnaglyphRune.anaglyphVertexShader, - AnaglyphRune.anaglyphFragmentShader - ); - gl.useProgram(shaderProgram); - const reduPt = gl.getUniformLocation(shaderProgram, 'u_sampler_red'); - const cyanuPt = gl.getUniformLocation(shaderProgram, 'u_sampler_cyan'); - const vertexPositionPointer = gl.getAttribLocation( - shaderProgram, - 'a_position' - ); - - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, leftBuffer.texture); - gl.uniform1i(cyanuPt, 0); - - gl.activeTexture(gl.TEXTURE1); - gl.bindTexture(gl.TEXTURE_2D, rightBuffer.texture); - gl.uniform1i(reduPt, 1); - - // draw a square, which will allow the texture to be used - // load position buffer - const positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, square.vertices, gl.STATIC_DRAW); - gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(vertexPositionPointer); - gl.drawArrays(gl.TRIANGLES, 0, 6); - }; -} - -/** - * Renders the specified Rune in a tab as an anaglyph. Use 3D glasses to view the - * anaglyph. - * @param rune - The Rune to render - * @return {Rune} The specified Rune - * - * @category Main - */ -export function anaglyph(rune: Rune): Rune { - throwIfNotRune('anaglyph', rune); - drawnRunes.push(new AnaglyphRune(rune)); - return rune; -} - -/** @hidden */ -export class HollusionRune extends DrawnRune { - constructor(rune: Rune, magnitude: number) { - super(rune, true); - this.rune.hollusionDistance = magnitude; - } - - private static readonly copyVertexShader = ` - precision mediump float; - attribute vec4 a_position; - varying highp vec2 v_texturePosition; - void main() { - gl_Position = a_position; - // texture position is in [0,1], vertex position is in [-1,1] - v_texturePosition.x = (a_position.x + 1.0) / 2.0; - v_texturePosition.y = (a_position.y + 1.0) / 2.0; - } - `; - - private static readonly copyFragmentShader = ` - precision mediump float; - uniform sampler2D uTexture; - varying highp vec2 v_texturePosition; - void main() { - gl_FragColor = texture2D(uTexture, v_texturePosition); - } - `; - - public draw = (canvas: HTMLCanvasElement) => { - const gl = getWebGlFromCanvas(canvas); - - const runes = white(overlay_frac(0.999999999, blank, scale(2.2, square))) - .flatten() - .concat(this.rune.flatten()); - - // first render all the frames into a framebuffer - const xshiftMax = runes[0].hollusionDistance; - const period = 2000; // animations loops every 2 seconds - const frameCount = 50; // in total 50 frames, gives rise to 25 fps - const frameBuffer: FrameBufferWithTexture[] = []; - - const renderFrame = (framePos: number): FrameBufferWithTexture => { - const fb = initFramebufferObject(gl); - // prepare camera projection array - const cameraMatrix = mat4.create(); - // let the object shift in the x direction - // the following calculation will let x oscillate in (-xshiftMax, xshiftMax) with time - let xshift = (framePos * (period / frameCount)) % period; - if (xshift > period / 2) { - xshift = period - xshift; - } - xshift = xshiftMax * (2 * ((2 * xshift) / period) - 1); - mat4.lookAt( - cameraMatrix, - vec3.fromValues(xshift, 0, 0), - vec3.fromValues(0, 0, -0.4), - vec3.fromValues(0, 1, 0) - ); - - drawRunesToFrameBuffer( - gl, - runes, - cameraMatrix, - new Float32Array([1, 1, 1, 1]), - fb.framebuffer, - true - ); - return fb; - }; - - for (let i = 0; i < frameCount; i += 1) { - frameBuffer.push(renderFrame(i)); - } - - // Then, draw a frame from framebuffer for each update - const copyShaderProgram = initShaderProgram( - gl, - HollusionRune.copyVertexShader, - HollusionRune.copyFragmentShader - ); - gl.useProgram(copyShaderProgram); - const texturePt = gl.getUniformLocation(copyShaderProgram, 'uTexture'); - const vertexPositionPointer = gl.getAttribLocation( - copyShaderProgram, - 'a_position' - ); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - const positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, square.vertices, gl.STATIC_DRAW); - gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(vertexPositionPointer); - - let lastTime = 0; - function render(timeInMs: number) { - if (timeInMs - lastTime < period / frameCount) return; - - lastTime = timeInMs; - - const framePos = - Math.floor(timeInMs / (period / frameCount)) % frameCount; - const fbObject = frameBuffer[framePos]; - gl.clearColor(1.0, 1.0, 1.0, 1.0); // Set clear color to white, fully opaque - // eslint-disable-next-line no-bitwise - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the viewport - - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, fbObject.texture); - gl.uniform1i(texturePt, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - } - - return render; - }; -} - -/** - * Renders the specified Rune in a tab as a hollusion, using the specified - * magnitude. - * @param rune - The Rune to render - * @param {number} magnitude - The hollusion's magnitude - * @return {Rune} The specified Rune - * - * @category Main - */ -export function hollusion_magnitude(rune: Rune, magnitude: number): Rune { - throwIfNotRune('hollusion_magnitude', rune); - drawnRunes.push(new HollusionRune(rune, magnitude)); - return rune; -} - -/** - * Renders the specified Rune in a tab as a hollusion, with a default magnitude - * of 0.1. - * @param rune - The Rune to render - * @return {Rune} The specified Rune - * - * @category Main - */ -export function hollusion(rune: Rune): Rune { - throwIfNotRune('hollusion', rune); - return hollusion_magnitude(rune, 0.1); -} - -/** - * Create an animation of runes - * @param duration Duration of the entire animation in seconds - * @param fps Duration of each frame in frames per seconds - * @param func Takes in the timestamp and returns a Rune to draw - * @returns A rune animation - * - * @category Main - */ -export function animate_rune( - duration: number, - fps: number, - func: RuneAnimation -) { - const anim = new AnimatedRune(duration, fps, (n) => { - const rune = func(n); - throwIfNotRune('animate_rune', rune); - return new NormalRune(rune); - }); - drawnRunes.push(anim); - return anim; -} - -/** - * Create an animation of anaglyph runes - * @param duration Duration of the entire animation in seconds - * @param fps Duration of each frame in frames per seconds - * @param func Takes in the timestamp and returns a Rune to draw - * @returns A rune animation - * - * @category Main - */ -export function animate_anaglyph( - duration: number, - fps: number, - func: RuneAnimation -) { - const anim = new AnimatedRune(duration, fps, (n) => { - const rune = func(n); - throwIfNotRune('animate_anaglyph', rune); - return new AnaglyphRune(rune); - }); - drawnRunes.push(anim); - return anim; -} +/* eslint-disable max-classes-per-file */ +/** + * The module `rune` provides functions for drawing runes. + * + * A *Rune* is defined by its vertices (x,y,z,t), the colors on its vertices (r,g,b,a), a transformation matrix for rendering the Rune and a (could be empty) list of its sub-Runes. + * @module rune + */ +import { mat4, vec3 } from 'gl-matrix'; +import { + Rune, + NormalRune, + RuneAnimation, + DrawnRune, + drawRunesToFrameBuffer, + AnimatedRune, +} from './rune'; +import { + getSquare, + getBlank, + getRcross, + getSail, + getTriangle, + getCorner, + getNova, + getCircle, + getHeart, + getPentagram, + getRibbon, + throwIfNotRune, + addColorFromHex, + colorPalette, + hexToColor, +} from './runes_ops'; +import { + FrameBufferWithTexture, + getWebGlFromCanvas, + initFramebufferObject, + initShaderProgram, +} from './runes_webgl'; + +/** @hidden */ +export const drawnRunes: (DrawnRune | AnimatedRune)[] = []; + +// ============================================================================= +// Basic Runes +// ============================================================================= + +/** + * Rune with the shape of a full square + * + * @category Primitive + */ +export const square: Rune = getSquare(); +/** + * Rune with the shape of a blank square + * + * @category Primitive + */ +export const blank: Rune = getBlank(); +/** + * Rune with the shape of a + * small square inside a large square, + * each diagonally split into a + * black and white half + * + * @category Primitive + */ +export const rcross: Rune = getRcross(); +/** + * Rune with the shape of a sail + * + * @category Primitive + */ +export const sail: Rune = getSail(); +/** + * Rune with the shape of a triangle + * + * @category Primitive + */ +export const triangle: Rune = getTriangle(); +/** + * Rune with black triangle, + * filling upper right corner + * + * @category Primitive + */ +export const corner: Rune = getCorner(); +/** + * Rune with the shape of two overlapping + * triangles, residing in the upper half + * of the shape + * + * @category Primitive + */ +export const nova: Rune = getNova(); +/** + * Rune with the shape of a circle + * + * @category Primitive + */ +export const circle: Rune = getCircle(); +/** + * Rune with the shape of a heart + * + * @category Primitive + */ +export const heart: Rune = getHeart(); +/** + * Rune with the shape of a pentagram + * + * @category Primitive + */ +export const pentagram: Rune = getPentagram(); +/** + * Rune with the shape of a ribbon + * winding outwards in an anticlockwise spiral + * + * @category Primitive + */ +export const ribbon: Rune = getRibbon(); + +// ============================================================================= +// Textured Runes +// ============================================================================= +/** + * Create a rune using the image provided in the url + * @param {string} imageUrl URL to the image that is used to create the rune. + * Note that the url must be from a domain that allows CORS. + * @returns {Rune} Rune created using the image. + * + * @category Main + */ +export function from_url(imageUrl: string): Rune { + const rune = getSquare(); + rune.texture = new Image(); + rune.texture.crossOrigin = 'anonymous'; + rune.texture.src = imageUrl; + return rune; +} + +// ============================================================================= +// XY-axis Transformation functions +// ============================================================================= + +/** + * Scales a given Rune by separate factors in x and y direction + * @param {number} ratio_x - Scaling factor in x direction + * @param {number} ratio_y - Scaling factor in y direction + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting scaled Rune + * + * @category Main + */ +export function scale_independent( + ratio_x: number, + ratio_y: number, + rune: Rune, +): Rune { + throwIfNotRune('scale_independent', rune); + const scaleVec = vec3.fromValues(ratio_x, ratio_y, 1); + const scaleMat = mat4.create(); + mat4.scale(scaleMat, scaleMat, scaleVec); + + const wrapperMat = mat4.create(); + mat4.multiply(wrapperMat, scaleMat, wrapperMat); + return Rune.of({ + subRunes: [rune], + transformMatrix: wrapperMat, + }); +} + +/** + * Scales a given Rune by a given factor in both x and y direction + * @param {number} ratio - Scaling factor + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting scaled Rune + * + * @category Main + */ +export function scale(ratio: number, rune: Rune): Rune { + throwIfNotRune('scale', rune); + return scale_independent(ratio, ratio, rune); +} + +/** + * Translates a given Rune by given values in x and y direction + * @param {number} x - Translation in x direction + * @param {number} y - Translation in y direction + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting translated Rune + * + * @category Main + */ +export function translate(x: number, y: number, rune: Rune): Rune { + throwIfNotRune('translate', rune); + const translateVec = vec3.fromValues(x, -y, 0); + const translateMat = mat4.create(); + mat4.translate(translateMat, translateMat, translateVec); + + const wrapperMat = mat4.create(); + mat4.multiply(wrapperMat, translateMat, wrapperMat); + return Rune.of({ + subRunes: [rune], + transformMatrix: wrapperMat, + }); +} + +/** + * Rotates a given Rune by a given angle, + * given in radians, in anti-clockwise direction. + * Note that parts of the Rune + * may be cropped as a result. + * @param {number} rad - Angle in radians + * @param {Rune} rune - Given Rune + * @return {Rune} Rotated Rune + * + * @category Main + */ +export function rotate(rad: number, rune: Rune): Rune { + throwIfNotRune('rotate', rune); + const rotateMat = mat4.create(); + mat4.rotateZ(rotateMat, rotateMat, rad); + + const wrapperMat = mat4.create(); + mat4.multiply(wrapperMat, rotateMat, wrapperMat); + return Rune.of({ + subRunes: [rune], + transformMatrix: wrapperMat, + }); +} + +/** + * Makes a new Rune from two given Runes by + * placing the first on top of the second + * such that the first one occupies frac + * portion of the height of the result and + * the second the rest + * @param {number} frac - Fraction between 0 and 1 (inclusive) + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function stack_frac(frac: number, rune1: Rune, rune2: Rune): Rune { + throwIfNotRune('stack_frac', rune1); + throwIfNotRune('stack_frac', rune2); + + if (!(frac >= 0 && frac <= 1)) { + throw Error('stack_frac can only take fraction in [0,1].'); + } + + const upper = translate(0, -(1 - frac), scale_independent(1, frac, rune1)); + const lower = translate(0, frac, scale_independent(1, 1 - frac, rune2)); + return Rune.of({ + subRunes: [upper, lower], + }); +} + +/** + * Makes a new Rune from two given Runes by + * placing the first on top of the second, each + * occupying equal parts of the height of the + * result + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function stack(rune1: Rune, rune2: Rune): Rune { + throwIfNotRune('stack', rune1, rune2); + return stack_frac(1 / 2, rune1, rune2); +} + +/** + * Makes a new Rune from a given Rune + * by vertically stacking n copies of it + * @param {number} n - Positive integer + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function stackn(n: number, rune: Rune): Rune { + throwIfNotRune('stackn', rune); + if (n === 1) { + return rune; + } + return stack_frac(1 / n, rune, stackn(n - 1, rune)); +} + +/** + * Makes a new Rune from a given Rune + * by turning it a quarter-turn around the centre in + * clockwise direction. + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function quarter_turn_right(rune: Rune): Rune { + throwIfNotRune('quarter_turn_right', rune); + return rotate(-Math.PI / 2, rune); +} + +/** + * Makes a new Rune from a given Rune + * by turning it a quarter-turn in + * anti-clockwise direction. + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function quarter_turn_left(rune: Rune): Rune { + throwIfNotRune('quarter_turn_left', rune); + return rotate(Math.PI / 2, rune); +} + +/** + * Makes a new Rune from a given Rune + * by turning it upside-down + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function turn_upside_down(rune: Rune): Rune { + throwIfNotRune('turn_upside_down', rune); + return rotate(Math.PI, rune); +} + +/** + * Makes a new Rune from two given Runes by + * placing the first on the left of the second + * such that the first one occupies frac + * portion of the width of the result and + * the second the rest + * @param {number} frac - Fraction between 0 and 1 (inclusive) + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function beside_frac(frac: number, rune1: Rune, rune2: Rune): Rune { + throwIfNotRune('beside_frac', rune1, rune2); + + if (!(frac >= 0 && frac <= 1)) { + throw Error('beside_frac can only take fraction in [0,1].'); + } + + const left = translate(-(1 - frac), 0, scale_independent(frac, 1, rune1)); + const right = translate(frac, 0, scale_independent(1 - frac, 1, rune2)); + return Rune.of({ + subRunes: [left, right], + }); +} + +/** + * Makes a new Rune from two given Runes by + * placing the first on the left of the second, + * both occupying equal portions of the width + * of the result + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function beside(rune1: Rune, rune2: Rune): Rune { + throwIfNotRune('beside', rune1, rune2); + return beside_frac(1 / 2, rune1, rune2); +} + +/** + * Makes a new Rune from a given Rune by + * flipping it around a horizontal axis, + * turning it upside down + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function flip_vert(rune: Rune): Rune { + throwIfNotRune('flip_vert', rune); + return scale_independent(1, -1, rune); +} + +/** + * Makes a new Rune from a given Rune by + * flipping it around a vertical axis, + * creating a mirror image + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function flip_horiz(rune: Rune): Rune { + throwIfNotRune('flip_horiz', rune); + return scale_independent(-1, 1, rune); +} + +/** + * Makes a new Rune from a given Rune by + * arranging into a square for copies of the + * given Rune in different orientations + * @param {Rune} rune - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function make_cross(rune: Rune): Rune { + throwIfNotRune('make_cross', rune); + return stack( + beside(quarter_turn_right(rune), rotate(Math.PI, rune)), + beside(rune, rotate(Math.PI / 2, rune)), + ); +} + +/** + * Applies a given function n times to an initial value + * @param {number} n - A non-negative integer + * @param {function} pattern - Unary function from Rune to Rune + * @param {Rune} initial - The initial Rune + * @return {Rune} - Result of n times application of pattern to initial: + * pattern(pattern(...pattern(pattern(initial))...)) + * + * @category Main + */ +export function repeat_pattern( + n: number, + pattern: (a: Rune) => Rune, + initial: Rune, +): Rune { + if (n === 0) { + return initial; + } + return pattern(repeat_pattern(n - 1, pattern, initial)); +} + +// ============================================================================= +// Z-axis Transformation functions +// ============================================================================= + +/** + * The depth range of the z-axis of a rune is [0,-1], this function gives a [0, -frac] of the depth range to rune1 and the rest to rune2. + * @param {number} frac - Fraction between 0 and 1 (inclusive) + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Rune + * + * @category Main + */ +export function overlay_frac(frac: number, rune1: Rune, rune2: Rune): Rune { + // to developer: please read https://www.tutorialspoint.com/webgl/webgl_basics.htm to understand the webgl z-axis interpretation. + // The key point is that positive z is closer to the screen. Hence, the image at the back should have smaller z value. Primitive runes have z = 0. + throwIfNotRune('overlay_frac', rune1); + throwIfNotRune('overlay_frac', rune2); + if (!(frac >= 0 && frac <= 1)) { + throw Error('overlay_frac can only take fraction in [0,1].'); + } + // by definition, when frac == 0 or 1, the back rune will overlap with the front rune. + // however, this would cause graphical glitch because overlapping is physically impossible + // we hack this problem by clipping the frac input from [0,1] to [1E-6, 1-1E-6] + // this should not be graphically noticable + let useFrac = frac; + const minFrac = 0.000001; + const maxFrac = 1 - minFrac; + if (useFrac < minFrac) { + useFrac = minFrac; + } + if (useFrac > maxFrac) { + useFrac = maxFrac; + } + + const frontMat = mat4.create(); + // z: scale by frac + mat4.scale(frontMat, frontMat, vec3.fromValues(1, 1, useFrac)); + const front = Rune.of({ + subRunes: [rune1], + transformMatrix: frontMat, + }); + + const backMat = mat4.create(); + // need to apply transformation in backwards order! + mat4.translate(backMat, backMat, vec3.fromValues(0, 0, -useFrac)); + mat4.scale(backMat, backMat, vec3.fromValues(1, 1, 1 - useFrac)); + const back = Rune.of({ + subRunes: [rune2], + transformMatrix: backMat, + }); + + return Rune.of({ + subRunes: [front, back], // render front first to avoid redrawing + }); +} + +/** + * The depth range of the z-axis of a rune is [0,-1], this function maps the depth range of rune1 and rune2 to [0,-0.5] and [-0.5,-1] respectively. + * @param {Rune} rune1 - Given Rune + * @param {Rune} rune2 - Given Rune + * @return {Rune} Resulting Runes + * + * @category Main + */ +export function overlay(rune1: Rune, rune2: Rune): Rune { + throwIfNotRune('overlay', rune1); + throwIfNotRune('overlay', rune2); + return overlay_frac(0.5, rune1, rune2); +} + +// ============================================================================= +// Color functions +// ============================================================================= + +/** + * Adds color to rune by specifying + * the red, green, blue (RGB) value, ranging from 0.0 to 1.0. + * RGB is additive: if all values are 1, the color is white, + * and if all values are 0, the color is black. + * @param {Rune} rune - The rune to add color to + * @param {number} r - Red value [0.0-1.0] + * @param {number} g - Green value [0.0-1.0] + * @param {number} b - Blue value [0.0-1.0] + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function color(rune: Rune, r: number, g: number, b: number): Rune { + throwIfNotRune('color', rune); + + const colorVector = [r, g, b, 1]; + return Rune.of({ + colors: new Float32Array(colorVector), + subRunes: [rune], + }); +} + +/** + * Gives random color to the given rune. + * The color is chosen randomly from the following nine + * colors: red, pink, purple, indigo, blue, green, yellow, orange, brown + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function random_color(rune: Rune): Rune { + throwIfNotRune('random_color', rune); + const randomColor = hexToColor( + colorPalette[Math.floor(Math.random() * colorPalette.length)], + ); + + return Rune.of({ + colors: new Float32Array(randomColor), + subRunes: [rune], + }); +} + +/** + * Colors the given rune red (#F44336). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function red(rune: Rune): Rune { + throwIfNotRune('red', rune); + return addColorFromHex(rune, '#F44336'); +} + +/** + * Colors the given rune pink (#E91E63s). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function pink(rune: Rune): Rune { + throwIfNotRune('pink', rune); + return addColorFromHex(rune, '#E91E63'); +} + +/** + * Colors the given rune purple (#AA00FF). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function purple(rune: Rune): Rune { + throwIfNotRune('purple', rune); + return addColorFromHex(rune, '#AA00FF'); +} + +/** + * Colors the given rune indigo (#3F51B5). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function indigo(rune: Rune): Rune { + throwIfNotRune('indigo', rune); + return addColorFromHex(rune, '#3F51B5'); +} + +/** + * Colors the given rune blue (#2196F3). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function blue(rune: Rune): Rune { + throwIfNotRune('blue', rune); + return addColorFromHex(rune, '#2196F3'); +} + +/** + * Colors the given rune green (#4CAF50). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function green(rune: Rune): Rune { + throwIfNotRune('green', rune); + return addColorFromHex(rune, '#4CAF50'); +} + +/** + * Colors the given rune yellow (#FFEB3B). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function yellow(rune: Rune): Rune { + throwIfNotRune('yellow', rune); + return addColorFromHex(rune, '#FFEB3B'); +} + +/** + * Colors the given rune orange (#FF9800). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function orange(rune: Rune): Rune { + throwIfNotRune('orange', rune); + return addColorFromHex(rune, '#FF9800'); +} + +/** + * Colors the given rune brown. + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function brown(rune: Rune): Rune { + throwIfNotRune('brown', rune); + return addColorFromHex(rune, '#795548'); +} + +/** + * Colors the given rune black (#000000). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function black(rune: Rune): Rune { + throwIfNotRune('black', rune); + return addColorFromHex(rune, '#000000'); +} + +/** + * Colors the given rune white (#FFFFFF). + * @param {Rune} rune - The rune to color + * @returns {Rune} The colored Rune + * + * @category Color + */ +export function white(rune: Rune): Rune { + throwIfNotRune('white', rune); + return addColorFromHex(rune, '#FFFFFF'); +} + +// ============================================================================= +// Drawing functions +// ============================================================================= + +/** + * Renders the specified Rune in a tab as a basic drawing. + * @param rune - The Rune to render + * @return {Rune} The specified Rune + * + * @category Main + */ +export function show(rune: Rune): Rune { + throwIfNotRune('show', rune); + drawnRunes.push(new NormalRune(rune)); + return rune; +} + +/** @hidden */ +export class AnaglyphRune extends DrawnRune { + private static readonly anaglyphVertexShader = ` + precision mediump float; + attribute vec4 a_position; + varying highp vec2 v_texturePosition; + void main() { + gl_Position = a_position; + // texture position is in [0,1], vertex position is in [-1,1] + v_texturePosition.x = (a_position.x + 1.0) / 2.0; + v_texturePosition.y = (a_position.y + 1.0) / 2.0; + } + `; + + private static readonly anaglyphFragmentShader = ` + precision mediump float; + uniform sampler2D u_sampler_red; + uniform sampler2D u_sampler_cyan; + varying highp vec2 v_texturePosition; + void main() { + gl_FragColor = texture2D(u_sampler_red, v_texturePosition) + + texture2D(u_sampler_cyan, v_texturePosition) - 1.0; + gl_FragColor.a = 1.0; + } + `; + + constructor(rune: Rune) { + super(rune, false); + } + + public draw = (canvas: HTMLCanvasElement) => { + const gl = getWebGlFromCanvas(canvas); + + // before draw the runes to framebuffer, we need to first draw a white background to cover the transparent places + const runes = white(overlay_frac(0.999999999, blank, scale(2.2, square))) + .flatten() + .concat(this.rune.flatten()); + + // calculate the left and right camera matrices + const halfEyeDistance = 0.03; + const leftCameraMatrix = mat4.create(); + mat4.lookAt( + leftCameraMatrix, + vec3.fromValues(-halfEyeDistance, 0, 0), + vec3.fromValues(0, 0, -0.4), + vec3.fromValues(0, 1, 0), + ); + const rightCameraMatrix = mat4.create(); + mat4.lookAt( + rightCameraMatrix, + vec3.fromValues(halfEyeDistance, 0, 0), + vec3.fromValues(0, 0, -0.4), + vec3.fromValues(0, 1, 0), + ); + + // left/right eye images are drawn into respective framebuffers + const leftBuffer = initFramebufferObject(gl); + const rightBuffer = initFramebufferObject(gl); + drawRunesToFrameBuffer( + gl, + runes, + leftCameraMatrix, + new Float32Array([1, 0, 0, 1]), + leftBuffer.framebuffer, + true, + ); + drawRunesToFrameBuffer( + gl, + runes, + rightCameraMatrix, + new Float32Array([0, 1, 1, 1]), + rightBuffer.framebuffer, + true, + ); + + // prepare to draw to screen by setting framebuffer to null + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + // prepare the shader program to combine the left/right eye images + const shaderProgram = initShaderProgram( + gl, + AnaglyphRune.anaglyphVertexShader, + AnaglyphRune.anaglyphFragmentShader, + ); + gl.useProgram(shaderProgram); + const reduPt = gl.getUniformLocation(shaderProgram, 'u_sampler_red'); + const cyanuPt = gl.getUniformLocation(shaderProgram, 'u_sampler_cyan'); + const vertexPositionPointer = gl.getAttribLocation( + shaderProgram, + 'a_position', + ); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, leftBuffer.texture); + gl.uniform1i(cyanuPt, 0); + + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, rightBuffer.texture); + gl.uniform1i(reduPt, 1); + + // draw a square, which will allow the texture to be used + // load position buffer + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, square.vertices, gl.STATIC_DRAW); + gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(vertexPositionPointer); + gl.drawArrays(gl.TRIANGLES, 0, 6); + }; +} + +/** + * Renders the specified Rune in a tab as an anaglyph. Use 3D glasses to view the + * anaglyph. + * @param rune - The Rune to render + * @return {Rune} The specified Rune + * + * @category Main + */ +export function anaglyph(rune: Rune): Rune { + throwIfNotRune('anaglyph', rune); + drawnRunes.push(new AnaglyphRune(rune)); + return rune; +} + +/** @hidden */ +export class HollusionRune extends DrawnRune { + constructor(rune: Rune, magnitude: number) { + super(rune, true); + this.rune.hollusionDistance = magnitude; + } + + private static readonly copyVertexShader = ` + precision mediump float; + attribute vec4 a_position; + varying highp vec2 v_texturePosition; + void main() { + gl_Position = a_position; + // texture position is in [0,1], vertex position is in [-1,1] + v_texturePosition.x = (a_position.x + 1.0) / 2.0; + v_texturePosition.y = (a_position.y + 1.0) / 2.0; + } + `; + + private static readonly copyFragmentShader = ` + precision mediump float; + uniform sampler2D uTexture; + varying highp vec2 v_texturePosition; + void main() { + gl_FragColor = texture2D(uTexture, v_texturePosition); + } + `; + + public draw = (canvas: HTMLCanvasElement) => { + const gl = getWebGlFromCanvas(canvas); + + const runes = white(overlay_frac(0.999999999, blank, scale(2.2, square))) + .flatten() + .concat(this.rune.flatten()); + + // first render all the frames into a framebuffer + const xshiftMax = runes[0].hollusionDistance; + const period = 2000; // animations loops every 2 seconds + const frameCount = 50; // in total 50 frames, gives rise to 25 fps + const frameBuffer: FrameBufferWithTexture[] = []; + + const renderFrame = (framePos: number): FrameBufferWithTexture => { + const fb = initFramebufferObject(gl); + // prepare camera projection array + const cameraMatrix = mat4.create(); + // let the object shift in the x direction + // the following calculation will let x oscillate in (-xshiftMax, xshiftMax) with time + let xshift = (framePos * (period / frameCount)) % period; + if (xshift > period / 2) { + xshift = period - xshift; + } + xshift = xshiftMax * (2 * ((2 * xshift) / period) - 1); + mat4.lookAt( + cameraMatrix, + vec3.fromValues(xshift, 0, 0), + vec3.fromValues(0, 0, -0.4), + vec3.fromValues(0, 1, 0), + ); + + drawRunesToFrameBuffer( + gl, + runes, + cameraMatrix, + new Float32Array([1, 1, 1, 1]), + fb.framebuffer, + true, + ); + return fb; + }; + + for (let i = 0; i < frameCount; i += 1) { + frameBuffer.push(renderFrame(i)); + } + + // Then, draw a frame from framebuffer for each update + const copyShaderProgram = initShaderProgram( + gl, + HollusionRune.copyVertexShader, + HollusionRune.copyFragmentShader, + ); + gl.useProgram(copyShaderProgram); + const texturePt = gl.getUniformLocation(copyShaderProgram, 'uTexture'); + const vertexPositionPointer = gl.getAttribLocation( + copyShaderProgram, + 'a_position', + ); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, square.vertices, gl.STATIC_DRAW); + gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(vertexPositionPointer); + + let lastTime = 0; + function render(timeInMs: number) { + if (timeInMs - lastTime < period / frameCount) return; + + lastTime = timeInMs; + + const framePos + = Math.floor(timeInMs / (period / frameCount)) % frameCount; + const fbObject = frameBuffer[framePos]; + gl.clearColor(1.0, 1.0, 1.0, 1.0); // Set clear color to white, fully opaque + // eslint-disable-next-line no-bitwise + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the viewport + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, fbObject.texture); + gl.uniform1i(texturePt, 0); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + return render; + }; +} + +/** + * Renders the specified Rune in a tab as a hollusion, using the specified + * magnitude. + * @param rune - The Rune to render + * @param {number} magnitude - The hollusion's magnitude + * @return {Rune} The specified Rune + * + * @category Main + */ +export function hollusion_magnitude(rune: Rune, magnitude: number): Rune { + throwIfNotRune('hollusion_magnitude', rune); + drawnRunes.push(new HollusionRune(rune, magnitude)); + return rune; +} + +/** + * Renders the specified Rune in a tab as a hollusion, with a default magnitude + * of 0.1. + * @param rune - The Rune to render + * @return {Rune} The specified Rune + * + * @category Main + */ +export function hollusion(rune: Rune): Rune { + throwIfNotRune('hollusion', rune); + return hollusion_magnitude(rune, 0.1); +} + +/** + * Create an animation of runes + * @param duration Duration of the entire animation in seconds + * @param fps Duration of each frame in frames per seconds + * @param func Takes in the timestamp and returns a Rune to draw + * @returns A rune animation + * + * @category Main + */ +export function animate_rune( + duration: number, + fps: number, + func: RuneAnimation, +) { + const anim = new AnimatedRune(duration, fps, (n) => { + const rune = func(n); + throwIfNotRune('animate_rune', rune); + return new NormalRune(rune); + }); + drawnRunes.push(anim); + return anim; +} + +/** + * Create an animation of anaglyph runes + * @param duration Duration of the entire animation in seconds + * @param fps Duration of each frame in frames per seconds + * @param func Takes in the timestamp and returns a Rune to draw + * @returns A rune animation + * + * @category Main + */ +export function animate_anaglyph( + duration: number, + fps: number, + func: RuneAnimation, +) { + const anim = new AnimatedRune(duration, fps, (n) => { + const rune = func(n); + throwIfNotRune('animate_anaglyph', rune); + return new AnaglyphRune(rune); + }); + drawnRunes.push(anim); + return anim; +} diff --git a/src/bundles/rune/index.ts b/src/bundles/rune/index.ts index e7d7465d9..65ce5d928 100644 --- a/src/bundles/rune/index.ts +++ b/src/bundles/rune/index.ts @@ -1,141 +1,141 @@ -import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; -import { - anaglyph, - animate_anaglyph, - animate_rune, - beside, - beside_frac, - black, - blank, - blue, - brown, - circle, - color, - corner, - drawnRunes, - flip_horiz, - flip_vert, - from_url, - green, - heart, - hollusion, - hollusion_magnitude, - indigo, - make_cross, - nova, - orange, - overlay, - overlay_frac, - pentagram, - pink, - purple, - quarter_turn_left, - quarter_turn_right, - random_color, - rcross, - red, - repeat_pattern, - ribbon, - rotate, - sail, - scale, - scale_independent, - show, - square, - stack, - stackn, - stack_frac, - translate, - triangle, - turn_upside_down, - white, - yellow, -} from './functions'; -import { RunesModuleState } from './rune'; - -/** - * Bundle for Source Academy Runes module - * @author Hou Ruomu - */ - -export default function runes( - moduleParams: ModuleParams, - moduleContexts: ModuleContexts -) { - // Update the module's global context - let moduleContext = moduleContexts.get('rune'); - - if (!moduleContext) { - moduleContext = { - tabs: [], - state: { - drawnRunes, - }, - }; - - moduleContexts.set('rune', moduleContext); - } else if (!moduleContext.state) { - moduleContext.state = { - drawnRunes, - }; - } else { - (moduleContext.state as RunesModuleState).drawnRunes = drawnRunes; - } - - return { - square, - blank, - rcross, - sail, - triangle, - corner, - nova, - circle, - heart, - pentagram, - ribbon, - - from_url, - - scale_independent, - scale, - translate, - rotate, - stack_frac, - stack, - stackn, - quarter_turn_left, - quarter_turn_right, - turn_upside_down, - beside_frac, - beside, - flip_vert, - flip_horiz, - make_cross, - repeat_pattern, - - overlay_frac, - overlay, - - color, - random_color, - red, - pink, - purple, - indigo, - blue, - green, - yellow, - orange, - brown, - black, - white, - - show, - anaglyph, - hollusion_magnitude, - hollusion, - animate_rune, - animate_anaglyph, - }; -} +import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; +import { + anaglyph, + animate_anaglyph, + animate_rune, + beside, + beside_frac, + black, + blank, + blue, + brown, + circle, + color, + corner, + drawnRunes, + flip_horiz, + flip_vert, + from_url, + green, + heart, + hollusion, + hollusion_magnitude, + indigo, + make_cross, + nova, + orange, + overlay, + overlay_frac, + pentagram, + pink, + purple, + quarter_turn_left, + quarter_turn_right, + random_color, + rcross, + red, + repeat_pattern, + ribbon, + rotate, + sail, + scale, + scale_independent, + show, + square, + stack, + stackn, + stack_frac, + translate, + triangle, + turn_upside_down, + white, + yellow, +} from './functions'; +import { RunesModuleState } from './rune'; + +/** + * Bundle for Source Academy Runes module + * @author Hou Ruomu + */ + +export default function runes( + moduleParams: ModuleParams, + moduleContexts: ModuleContexts, +) { + // Update the module's global context + let moduleContext = moduleContexts.get('rune'); + + if (!moduleContext) { + moduleContext = { + tabs: [], + state: { + drawnRunes, + }, + }; + + moduleContexts.set('rune', moduleContext); + } else if (!moduleContext.state) { + moduleContext.state = { + drawnRunes, + }; + } else { + (moduleContext.state as RunesModuleState).drawnRunes = drawnRunes; + } + + return { + square, + blank, + rcross, + sail, + triangle, + corner, + nova, + circle, + heart, + pentagram, + ribbon, + + from_url, + + scale_independent, + scale, + translate, + rotate, + stack_frac, + stack, + stackn, + quarter_turn_left, + quarter_turn_right, + turn_upside_down, + beside_frac, + beside, + flip_vert, + flip_horiz, + make_cross, + repeat_pattern, + + overlay_frac, + overlay, + + color, + random_color, + red, + pink, + purple, + indigo, + blue, + green, + yellow, + orange, + brown, + black, + white, + + show, + anaglyph, + hollusion_magnitude, + hollusion, + animate_rune, + animate_anaglyph, + }; +} diff --git a/src/bundles/rune/rune.ts b/src/bundles/rune/rune.ts index 3f1ee9e1e..c3596b8b6 100644 --- a/src/bundles/rune/rune.ts +++ b/src/bundles/rune/rune.ts @@ -1,420 +1,418 @@ -/* eslint-disable max-classes-per-file */ -import { mat4 } from 'gl-matrix'; -import { ModuleState } from 'js-slang'; -import { AnimFrame, glAnimation } from '../../typings/anim_types'; -import { ReplResult } from '../../typings/type_helpers'; -import { getWebGlFromCanvas, initShaderProgram } from './runes_webgl'; - -const normalVertexShader = ` -attribute vec4 aVertexPosition; -uniform vec4 uVertexColor; -uniform mat4 uModelViewMatrix; -uniform mat4 uProjectionMatrix; -uniform mat4 uCameraMatrix; - -varying lowp vec4 vColor; -varying highp vec2 vTexturePosition; -varying lowp float colorFactor; -void main(void) { - gl_Position = uProjectionMatrix * uCameraMatrix * uModelViewMatrix * aVertexPosition; - vColor = uVertexColor; - - // texture position is in [0,1], vertex position is in [-1,1] - vTexturePosition.x = (aVertexPosition.x + 1.0) / 2.0; - vTexturePosition.y = 1.0 - (aVertexPosition.y + 1.0) / 2.0; - - colorFactor = gl_Position.z; -} -`; - -const normalFragmentShader = ` -precision mediump float; -uniform bool uRenderWithTexture; -uniform bool uRenderWithDepthColor; -uniform sampler2D uTexture; -varying lowp float colorFactor; -uniform vec4 uColorFilter; - - -varying lowp vec4 vColor; -varying highp vec2 vTexturePosition; -void main(void) { - if (uRenderWithTexture){ - gl_FragColor = texture2D(uTexture, vTexturePosition); - } else { - gl_FragColor = vColor; - } - if (uRenderWithDepthColor){ - gl_FragColor += (colorFactor + 0.5) * (1.0 - gl_FragColor); - gl_FragColor.a = 1.0; - } - gl_FragColor = uColorFilter * gl_FragColor + 1.0 - uColorFilter; - gl_FragColor.a = 1.0; -} -`; -/** - * The basic data-representation of a Rune. When the Rune is drawn, every 3 consecutive vertex will form a triangle. - * @field vertices - A list of vertex coordinates, each vertex has 4 coordiante (x,y,z,t). - * @field colors - A list of vertex colors, each vertex has a color (r,g,b,a). - * @field transformMatrix - A mat4 that is applied to all the vertices and the sub runes - * @field subRune - A (potentially empty) list of Runes - */ -export class Rune { - constructor( - public vertices: Float32Array, - public colors: Float32Array | null, - public transformMatrix: mat4, - public subRunes: Rune[], - public texture: HTMLImageElement | null, - public hollusionDistance: number - ) {} - - public copy = () => - new Rune( - this.vertices, - this.colors, - mat4.clone(this.transformMatrix), - this.subRunes, - this.texture, - this.hollusionDistance - ); - - /** - * Flatten the subrunes to return a list of runes - * @return Rune[], a list of runes - */ - public flatten = () => { - const runeList: Rune[] = []; - const runeTodoList: Rune[] = [this.copy()]; - - while (runeTodoList.length !== 0) { - const runeToExpand: Rune = runeTodoList.pop()!; // ! claims that the pop() will not return undefined. - runeToExpand.subRunes.forEach((subRune: Rune) => { - const subRuneCopy = subRune.copy(); - - mat4.multiply( - subRuneCopy.transformMatrix, - runeToExpand.transformMatrix, - subRuneCopy.transformMatrix - ); - subRuneCopy.hollusionDistance = runeToExpand.hollusionDistance; - if (runeToExpand.colors !== null) { - subRuneCopy.colors = runeToExpand.colors; - } - runeTodoList.push(subRuneCopy); - }); - runeToExpand.subRunes = []; - if (runeToExpand.vertices.length > 0) { - runeList.push(runeToExpand); - } - } - return runeList; - }; - - public static of = ( - params: { - vertices?: Float32Array; - colors?: Float32Array | null; - transformMatrix?: mat4; - subRunes?: Rune[]; - texture?: HTMLImageElement | null; - hollusionDistance?: number; - } = {} - ) => { - const paramGetter = (name: string, defaultValue: () => any) => - params[name] === undefined ? defaultValue() : params[name]; - - return new Rune( - paramGetter('vertices', () => new Float32Array()), - paramGetter('colors', () => null), - paramGetter('transformMatrix', mat4.create), - paramGetter('subRunes', () => []), - paramGetter('texture', () => null), - paramGetter('hollusionDistance', () => 0.1) - ); - }; - - public toReplString = () => ''; -} - -/** - * Draws the list of runes with the prepared WebGLRenderingContext, with each rune overlapping each other onto a given framebuffer. if the framebuffer is null, draw to the default canvas. - * - * @param gl a prepared WebGLRenderingContext with shader program linked - * @param runes a list of rune (Rune[]) to be drawn sequentially - */ -export function drawRunesToFrameBuffer( - gl: WebGLRenderingContext, - runes: Rune[], - cameraMatrix: mat4, - colorFilter: Float32Array, - framebuffer: WebGLFramebuffer | null = null, - depthSwitch: boolean = false -) { - // step 1: initiate the WebGLRenderingContext - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - // step 2: initiate the shaderProgram - const shaderProgram = initShaderProgram( - gl, - normalVertexShader, - normalFragmentShader - ); - gl.useProgram(shaderProgram); - if (gl === null) { - throw Error('Rendering Context not initialized for drawRune.'); - } - - // create pointers to the data-entries of the shader program - const vertexPositionPointer = gl.getAttribLocation( - shaderProgram, - 'aVertexPosition' - ); - const vertexColorPointer = gl.getUniformLocation( - shaderProgram, - 'uVertexColor' - ); - const vertexColorFilterPt = gl.getUniformLocation( - shaderProgram, - 'uColorFilter' - ); - const projectionMatrixPointer = gl.getUniformLocation( - shaderProgram, - 'uProjectionMatrix' - ); - const cameraMatrixPointer = gl.getUniformLocation( - shaderProgram, - 'uCameraMatrix' - ); - const modelViewMatrixPointer = gl.getUniformLocation( - shaderProgram, - 'uModelViewMatrix' - ); - const textureSwitchPointer = gl.getUniformLocation( - shaderProgram, - 'uRenderWithTexture' - ); - const depthSwitchPointer = gl.getUniformLocation( - shaderProgram, - 'uRenderWithDepthColor' - ); - const texturePointer = gl.getUniformLocation(shaderProgram, 'uTexture'); - - // load depth - gl.uniform1i(depthSwitchPointer, depthSwitch ? 1 : 0); - - // load projection and camera - const orthoCam = mat4.create(); - mat4.ortho(orthoCam, -1, 1, -1, 1, -0.5, 1.5); - gl.uniformMatrix4fv(projectionMatrixPointer, false, orthoCam); - gl.uniformMatrix4fv(cameraMatrixPointer, false, cameraMatrix); - - // load colorfilter - gl.uniform4fv(vertexColorFilterPt, colorFilter); - - // 3. draw each Rune using the shader program - /** - * Credit to: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL - * Initialize a texture and load an image. - * When the image finished loading copy it into the texture. - */ - const loadTexture = (image: HTMLImageElement): WebGLTexture | null => { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - function isPowerOf2(value) { - // eslint-disable-next-line no-bitwise - return (value & (value - 1)) === 0; - } - // Because images have to be downloaded over the internet - // they might take a moment until they are ready. - // Until then put a single pixel in the texture so we can - // use it immediately. When the image has finished downloading - // we'll update the texture with the contents of the image. - const level = 0; - const internalFormat = gl.RGBA; - const width = 1; - const height = 1; - const border = 0; - const srcFormat = gl.RGBA; - const srcType = gl.UNSIGNED_BYTE; - const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - width, - height, - border, - srcFormat, - srcType, - pixel - ); - - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - srcFormat, - srcType, - image - ); - - // WebGL1 has different requirements for power of 2 images - // vs non power of 2 images so check if the image is a - // power of 2 in both dimensions. - if (isPowerOf2(image.width) && isPowerOf2(image.height)) { - // Yes, it's a power of 2. Generate mips. - gl.generateMipmap(gl.TEXTURE_2D); - } else { - // No, it's not a power of 2. Turn off mips and set - // wrapping to clamp to edge - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - } - - return texture; - }; - - runes.forEach((rune: Rune) => { - // load position buffer - const positionBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, rune.vertices, gl.STATIC_DRAW); - gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(vertexPositionPointer); - - // load color/texture - if (rune.texture === null) { - gl.uniform4fv( - vertexColorPointer, - rune.colors || new Float32Array([0, 0, 0, 1]) - ); - gl.uniform1i(textureSwitchPointer, 0); - } else { - const texture = loadTexture(rune.texture); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.uniform1i(texturePointer, 0); - gl.uniform1i(textureSwitchPointer, 1); - } - - // load transformation matrix - gl.uniformMatrix4fv(modelViewMatrixPointer, false, rune.transformMatrix); - - // draw - const vertexCount = rune.vertices.length / 4; - gl.drawArrays(gl.TRIANGLES, 0, vertexCount); - }); -} - -/** - * Represents runes with a draw method attached - */ -export abstract class DrawnRune implements ReplResult { - private static readonly normalVertexShader = ` - attribute vec4 aVertexPosition; - uniform vec4 uVertexColor; - uniform mat4 uModelViewMatrix; - uniform mat4 uProjectionMatrix; - uniform mat4 uCameraMatrix; - - varying lowp vec4 vColor; - varying highp vec2 vTexturePosition; - varying lowp float colorFactor; - void main(void) { - gl_Position = uProjectionMatrix * uCameraMatrix * uModelViewMatrix * aVertexPosition; - vColor = uVertexColor; - - // texture position is in [0,1], vertex position is in [-1,1] - vTexturePosition.x = (aVertexPosition.x + 1.0) / 2.0; - vTexturePosition.y = 1.0 - (aVertexPosition.y + 1.0) / 2.0; - - colorFactor = gl_Position.z; - } - `; - - private static readonly normalFragmentShader = ` - precision mediump float; - uniform bool uRenderWithTexture; - uniform bool uRenderWithDepthColor; - uniform sampler2D uTexture; - varying lowp float colorFactor; - uniform vec4 uColorFilter; - - - varying lowp vec4 vColor; - varying highp vec2 vTexturePosition; - void main(void) { - if (uRenderWithTexture){ - gl_FragColor = texture2D(uTexture, vTexturePosition); - } else { - gl_FragColor = vColor; - } - if (uRenderWithDepthColor){ - gl_FragColor += (colorFactor + 0.5) * (1.0 - gl_FragColor); - gl_FragColor.a = 1.0; - } - gl_FragColor = uColorFilter * gl_FragColor + 1.0 - uColorFilter; - gl_FragColor.a = 1.0; - } - `; - - constructor( - protected readonly rune: Rune, - public readonly isHollusion: boolean - ) {} - - public toReplString = () => ''; - - public abstract draw: (canvas: HTMLCanvasElement) => void; -} - -export class NormalRune extends DrawnRune { - constructor(rune: Rune) { - super(rune, false); - } - - public draw = (canvas: HTMLCanvasElement) => { - const gl = getWebGlFromCanvas(canvas); - - // prepare camera projection array - const cameraMatrix = mat4.create(); - - // color filter set to [1,1,1,1] for transparent filter - drawRunesToFrameBuffer( - gl, - this.rune.flatten(), - cameraMatrix, - new Float32Array([1, 1, 1, 1]), - null, - true - ); - }; -} - -/** A function that takes in a timestamp and returns a Rune */ -export type RuneAnimation = (time: number) => Rune; - -export class AnimatedRune extends glAnimation implements ReplResult { - constructor( - duration: number, - fps: number, - private readonly func: (frame: number) => DrawnRune - ) { - super(duration, fps); - } - - public getFrame(num: number): AnimFrame { - const rune = this.func(num); - return { - draw: rune.draw, - }; - } - - public toReplString = () => ''; -} - -export class RunesModuleState implements ModuleState { - constructor(public drawnRunes: (DrawnRune | AnimatedRune)[] = []) {} -} +/* eslint-disable max-classes-per-file */ +import { mat4 } from 'gl-matrix'; +import { ModuleState } from 'js-slang'; +import { AnimFrame, glAnimation } from '../../typings/anim_types'; +import { ReplResult } from '../../typings/type_helpers'; +import { getWebGlFromCanvas, initShaderProgram } from './runes_webgl'; + +const normalVertexShader = ` +attribute vec4 aVertexPosition; +uniform vec4 uVertexColor; +uniform mat4 uModelViewMatrix; +uniform mat4 uProjectionMatrix; +uniform mat4 uCameraMatrix; + +varying lowp vec4 vColor; +varying highp vec2 vTexturePosition; +varying lowp float colorFactor; +void main(void) { + gl_Position = uProjectionMatrix * uCameraMatrix * uModelViewMatrix * aVertexPosition; + vColor = uVertexColor; + + // texture position is in [0,1], vertex position is in [-1,1] + vTexturePosition.x = (aVertexPosition.x + 1.0) / 2.0; + vTexturePosition.y = 1.0 - (aVertexPosition.y + 1.0) / 2.0; + + colorFactor = gl_Position.z; +} +`; + +const normalFragmentShader = ` +precision mediump float; +uniform bool uRenderWithTexture; +uniform bool uRenderWithDepthColor; +uniform sampler2D uTexture; +varying lowp float colorFactor; +uniform vec4 uColorFilter; + + +varying lowp vec4 vColor; +varying highp vec2 vTexturePosition; +void main(void) { + if (uRenderWithTexture){ + gl_FragColor = texture2D(uTexture, vTexturePosition); + } else { + gl_FragColor = vColor; + } + if (uRenderWithDepthColor){ + gl_FragColor += (colorFactor + 0.5) * (1.0 - gl_FragColor); + gl_FragColor.a = 1.0; + } + gl_FragColor = uColorFilter * gl_FragColor + 1.0 - uColorFilter; + gl_FragColor.a = 1.0; +} +`; +/** + * The basic data-representation of a Rune. When the Rune is drawn, every 3 consecutive vertex will form a triangle. + * @field vertices - A list of vertex coordinates, each vertex has 4 coordiante (x,y,z,t). + * @field colors - A list of vertex colors, each vertex has a color (r,g,b,a). + * @field transformMatrix - A mat4 that is applied to all the vertices and the sub runes + * @field subRune - A (potentially empty) list of Runes + */ +export class Rune { + constructor( + public vertices: Float32Array, + public colors: Float32Array | null, + public transformMatrix: mat4, + public subRunes: Rune[], + public texture: HTMLImageElement | null, + public hollusionDistance: number, + ) {} + + public copy = () => new Rune( + this.vertices, + this.colors, + mat4.clone(this.transformMatrix), + this.subRunes, + this.texture, + this.hollusionDistance, + ); + + /** + * Flatten the subrunes to return a list of runes + * @return Rune[], a list of runes + */ + public flatten = () => { + const runeList: Rune[] = []; + const runeTodoList: Rune[] = [this.copy()]; + + while (runeTodoList.length !== 0) { + const runeToExpand: Rune = runeTodoList.pop()!; // ! claims that the pop() will not return undefined. + runeToExpand.subRunes.forEach((subRune: Rune) => { + const subRuneCopy = subRune.copy(); + + mat4.multiply( + subRuneCopy.transformMatrix, + runeToExpand.transformMatrix, + subRuneCopy.transformMatrix, + ); + subRuneCopy.hollusionDistance = runeToExpand.hollusionDistance; + if (runeToExpand.colors !== null) { + subRuneCopy.colors = runeToExpand.colors; + } + runeTodoList.push(subRuneCopy); + }); + runeToExpand.subRunes = []; + if (runeToExpand.vertices.length > 0) { + runeList.push(runeToExpand); + } + } + return runeList; + }; + + public static of = ( + params: { + vertices?: Float32Array; + colors?: Float32Array | null; + transformMatrix?: mat4; + subRunes?: Rune[]; + texture?: HTMLImageElement | null; + hollusionDistance?: number; + } = {}, + ) => { + const paramGetter = (name: string, defaultValue: () => any) => (params[name] === undefined ? defaultValue() : params[name]); + + return new Rune( + paramGetter('vertices', () => new Float32Array()), + paramGetter('colors', () => null), + paramGetter('transformMatrix', mat4.create), + paramGetter('subRunes', () => []), + paramGetter('texture', () => null), + paramGetter('hollusionDistance', () => 0.1), + ); + }; + + public toReplString = () => ''; +} + +/** + * Draws the list of runes with the prepared WebGLRenderingContext, with each rune overlapping each other onto a given framebuffer. if the framebuffer is null, draw to the default canvas. + * + * @param gl a prepared WebGLRenderingContext with shader program linked + * @param runes a list of rune (Rune[]) to be drawn sequentially + */ +export function drawRunesToFrameBuffer( + gl: WebGLRenderingContext, + runes: Rune[], + cameraMatrix: mat4, + colorFilter: Float32Array, + framebuffer: WebGLFramebuffer | null = null, + depthSwitch: boolean = false, +) { + // step 1: initiate the WebGLRenderingContext + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + // step 2: initiate the shaderProgram + const shaderProgram = initShaderProgram( + gl, + normalVertexShader, + normalFragmentShader, + ); + gl.useProgram(shaderProgram); + if (gl === null) { + throw Error('Rendering Context not initialized for drawRune.'); + } + + // create pointers to the data-entries of the shader program + const vertexPositionPointer = gl.getAttribLocation( + shaderProgram, + 'aVertexPosition', + ); + const vertexColorPointer = gl.getUniformLocation( + shaderProgram, + 'uVertexColor', + ); + const vertexColorFilterPt = gl.getUniformLocation( + shaderProgram, + 'uColorFilter', + ); + const projectionMatrixPointer = gl.getUniformLocation( + shaderProgram, + 'uProjectionMatrix', + ); + const cameraMatrixPointer = gl.getUniformLocation( + shaderProgram, + 'uCameraMatrix', + ); + const modelViewMatrixPointer = gl.getUniformLocation( + shaderProgram, + 'uModelViewMatrix', + ); + const textureSwitchPointer = gl.getUniformLocation( + shaderProgram, + 'uRenderWithTexture', + ); + const depthSwitchPointer = gl.getUniformLocation( + shaderProgram, + 'uRenderWithDepthColor', + ); + const texturePointer = gl.getUniformLocation(shaderProgram, 'uTexture'); + + // load depth + gl.uniform1i(depthSwitchPointer, depthSwitch ? 1 : 0); + + // load projection and camera + const orthoCam = mat4.create(); + mat4.ortho(orthoCam, -1, 1, -1, 1, -0.5, 1.5); + gl.uniformMatrix4fv(projectionMatrixPointer, false, orthoCam); + gl.uniformMatrix4fv(cameraMatrixPointer, false, cameraMatrix); + + // load colorfilter + gl.uniform4fv(vertexColorFilterPt, colorFilter); + + // 3. draw each Rune using the shader program + /** + * Credit to: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL + * Initialize a texture and load an image. + * When the image finished loading copy it into the texture. + */ + const loadTexture = (image: HTMLImageElement): WebGLTexture | null => { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + function isPowerOf2(value) { + // eslint-disable-next-line no-bitwise + return (value & (value - 1)) === 0; + } + // Because images have to be downloaded over the internet + // they might take a moment until they are ready. + // Until then put a single pixel in the texture so we can + // use it immediately. When the image has finished downloading + // we'll update the texture with the contents of the image. + const level = 0; + const internalFormat = gl.RGBA; + const width = 1; + const height = 1; + const border = 0; + const srcFormat = gl.RGBA; + const srcType = gl.UNSIGNED_BYTE; + const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue + gl.texImage2D( + gl.TEXTURE_2D, + level, + internalFormat, + width, + height, + border, + srcFormat, + srcType, + pixel, + ); + + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + level, + internalFormat, + srcFormat, + srcType, + image, + ); + + // WebGL1 has different requirements for power of 2 images + // vs non power of 2 images so check if the image is a + // power of 2 in both dimensions. + if (isPowerOf2(image.width) && isPowerOf2(image.height)) { + // Yes, it's a power of 2. Generate mips. + gl.generateMipmap(gl.TEXTURE_2D); + } else { + // No, it's not a power of 2. Turn off mips and set + // wrapping to clamp to edge + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + } + + return texture; + }; + + runes.forEach((rune: Rune) => { + // load position buffer + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, rune.vertices, gl.STATIC_DRAW); + gl.vertexAttribPointer(vertexPositionPointer, 4, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(vertexPositionPointer); + + // load color/texture + if (rune.texture === null) { + gl.uniform4fv( + vertexColorPointer, + rune.colors || new Float32Array([0, 0, 0, 1]), + ); + gl.uniform1i(textureSwitchPointer, 0); + } else { + const texture = loadTexture(rune.texture); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i(texturePointer, 0); + gl.uniform1i(textureSwitchPointer, 1); + } + + // load transformation matrix + gl.uniformMatrix4fv(modelViewMatrixPointer, false, rune.transformMatrix); + + // draw + const vertexCount = rune.vertices.length / 4; + gl.drawArrays(gl.TRIANGLES, 0, vertexCount); + }); +} + +/** + * Represents runes with a draw method attached + */ +export abstract class DrawnRune implements ReplResult { + private static readonly normalVertexShader = ` + attribute vec4 aVertexPosition; + uniform vec4 uVertexColor; + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + uniform mat4 uCameraMatrix; + + varying lowp vec4 vColor; + varying highp vec2 vTexturePosition; + varying lowp float colorFactor; + void main(void) { + gl_Position = uProjectionMatrix * uCameraMatrix * uModelViewMatrix * aVertexPosition; + vColor = uVertexColor; + + // texture position is in [0,1], vertex position is in [-1,1] + vTexturePosition.x = (aVertexPosition.x + 1.0) / 2.0; + vTexturePosition.y = 1.0 - (aVertexPosition.y + 1.0) / 2.0; + + colorFactor = gl_Position.z; + } + `; + + private static readonly normalFragmentShader = ` + precision mediump float; + uniform bool uRenderWithTexture; + uniform bool uRenderWithDepthColor; + uniform sampler2D uTexture; + varying lowp float colorFactor; + uniform vec4 uColorFilter; + + + varying lowp vec4 vColor; + varying highp vec2 vTexturePosition; + void main(void) { + if (uRenderWithTexture){ + gl_FragColor = texture2D(uTexture, vTexturePosition); + } else { + gl_FragColor = vColor; + } + if (uRenderWithDepthColor){ + gl_FragColor += (colorFactor + 0.5) * (1.0 - gl_FragColor); + gl_FragColor.a = 1.0; + } + gl_FragColor = uColorFilter * gl_FragColor + 1.0 - uColorFilter; + gl_FragColor.a = 1.0; + } + `; + + constructor( + protected readonly rune: Rune, + public readonly isHollusion: boolean, + ) {} + + public toReplString = () => ''; + + public abstract draw: (canvas: HTMLCanvasElement) => void; +} + +export class NormalRune extends DrawnRune { + constructor(rune: Rune) { + super(rune, false); + } + + public draw = (canvas: HTMLCanvasElement) => { + const gl = getWebGlFromCanvas(canvas); + + // prepare camera projection array + const cameraMatrix = mat4.create(); + + // color filter set to [1,1,1,1] for transparent filter + drawRunesToFrameBuffer( + gl, + this.rune.flatten(), + cameraMatrix, + new Float32Array([1, 1, 1, 1]), + null, + true, + ); + }; +} + +/** A function that takes in a timestamp and returns a Rune */ +export type RuneAnimation = (time: number) => Rune; + +export class AnimatedRune extends glAnimation implements ReplResult { + constructor( + duration: number, + fps: number, + private readonly func: (frame: number) => DrawnRune, + ) { + super(duration, fps); + } + + public getFrame(num: number): AnimFrame { + const rune = this.func(num); + return { + draw: rune.draw, + }; + } + + public toReplString = () => ''; +} + +export class RunesModuleState implements ModuleState { + constructor(public drawnRunes: (DrawnRune | AnimatedRune)[] = []) {} +} diff --git a/src/bundles/rune/runes_ops.ts b/src/bundles/rune/runes_ops.ts index e7066351d..3992f4d2b 100644 --- a/src/bundles/rune/runes_ops.ts +++ b/src/bundles/rune/runes_ops.ts @@ -1,360 +1,360 @@ -/** - * This file contains the bundle's private functions for runes. - */ -import { Rune } from './rune'; - -// ============================================================================= -// Utility Functions -// ============================================================================= -export function throwIfNotRune(name, ...runes) { - runes.forEach((rune) => { - if (!(rune instanceof Rune)) { - throw Error(`${name} expects a rune as argument.`); - } - }); -} - -// ============================================================================= -// Basic Runes -// ============================================================================= - -/** - * primitive Rune in the rune of a full square - * */ -export const getSquare: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - vertexList.push(-1, 1, 0, 1); - vertexList.push(-1, -1, 0, 1); - vertexList.push(1, -1, 0, 1); - vertexList.push(1, -1, 0, 1); - vertexList.push(-1, 1, 0, 1); - vertexList.push(1, 1, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -export const getBlank: () => Rune = () => Rune.of(); - -/** - * primitive Rune in the rune of a - * smallsquare inside a large square, - * each diagonally split into a - * black and white half - * */ -export const getRcross: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - // lower small triangle - vertexList.push(-0.5, 0.5, 0, 1); - vertexList.push(-0.5, -0.5, 0, 1); - vertexList.push(0.5, -0.5, 0, 1); - - // upper shape, starting from left-top corner - vertexList.push(-1, 1, 0, 1); - vertexList.push(-0.5, 0.5, 0, 1); - vertexList.push(1, 1, 0, 1); - - vertexList.push(-0.5, 0.5, 0, 1); - vertexList.push(1, 1, 0, 1); - vertexList.push(0.5, 0.5, 0, 1); - - vertexList.push(1, 1, 0, 1); - vertexList.push(0.5, 0.5, 0, 1); - vertexList.push(1, -1, 0, 1); - - vertexList.push(0.5, 0.5, 0, 1); - vertexList.push(1, -1, 0, 1); - vertexList.push(0.5, -0.5, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a sail - * */ -export const getSail: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - vertexList.push(0.5, -1, 0, 1); - vertexList.push(0, -1, 0, 1); - vertexList.push(0, 1, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a triangle - * */ -export const getTriangle: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - vertexList.push(1, -1, 0, 1); - vertexList.push(0, -1, 0, 1); - vertexList.push(0, 1, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune with black triangle, - * filling upper right corner - * */ -export const getCorner: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - vertexList.push(1, 0, 0, 1); - vertexList.push(1, 1, 0, 1); - vertexList.push(0, 1, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of two overlapping - * triangles, residing in the upper half - * of - * */ -export const getNova: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - vertexList.push(0, 1, 0, 1); - vertexList.push(-0.5, 0, 0, 1); - vertexList.push(0, 0.5, 0, 1); - - vertexList.push(-0.5, 0, 0, 1); - vertexList.push(0, 0.5, 0, 1); - vertexList.push(1, 0, 0, 1); - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a circle - * */ -export const getCircle: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - const circleDiv = 60; - for (let i = 0; i < circleDiv; i += 1) { - const angle1 = ((2 * Math.PI) / circleDiv) * i; - const angle2 = ((2 * Math.PI) / circleDiv) * (i + 1); - vertexList.push(Math.cos(angle1), Math.sin(angle1), 0, 1); - vertexList.push(Math.cos(angle2), Math.sin(angle2), 0, 1); - vertexList.push(0, 0, 0, 1); - } - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a heart - * */ -export const getHeart: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - const root2 = Math.sqrt(2); - const r = 4 / (2 + 3 * root2); - const scaleX = 1 / (r * (1 + root2 / 2)); - const numPoints = 10; - - // right semi-circle - const rightCenterX = r / root2; - const rightCenterY = 1 - r; - for (let i = 0; i < numPoints; i += 1) { - const angle1 = Math.PI * (-1 / 4 + i / numPoints); - const angle2 = Math.PI * (-1 / 4 + (i + 1) / numPoints); - vertexList.push( - (Math.cos(angle1) * r + rightCenterX) * scaleX, - Math.sin(angle1) * r + rightCenterY, - 0, - 1 - ); - vertexList.push( - (Math.cos(angle2) * r + rightCenterX) * scaleX, - Math.sin(angle2) * r + rightCenterY, - 0, - 1 - ); - vertexList.push(0, -1, 0, 1); - } - // left semi-circle - const leftCenterX = -r / root2; - const leftCenterY = 1 - r; - for (let i = 0; i <= numPoints; i += 1) { - const angle1 = Math.PI * (1 / 4 + i / numPoints); - const angle2 = Math.PI * (1 / 4 + (i + 1) / numPoints); - vertexList.push( - (Math.cos(angle1) * r + leftCenterX) * scaleX, - Math.sin(angle1) * r + leftCenterY, - 0, - 1 - ); - vertexList.push( - (Math.cos(angle2) * r + leftCenterX) * scaleX, - Math.sin(angle2) * r + leftCenterY, - 0, - 1 - ); - vertexList.push(0, -1, 0, 1); - } - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a pentagram - * */ -export const getPentagram: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - const v1 = Math.sin(Math.PI / 10); - const v2 = Math.cos(Math.PI / 10); - - const w1 = Math.sin((3 * Math.PI) / 10); - const w2 = Math.cos((3 * Math.PI) / 10); - - const vertices: number[][] = []; - vertices.push([v2, v1, 0, 1]); - vertices.push([w2, -w1, 0, 1]); - vertices.push([-w2, -w1, 0, 1]); - vertices.push([-v2, v1, 0, 1]); - vertices.push([0, 1, 0, 1]); - - for (let i = 0; i < 5; i += 1) { - vertexList.push(0, 0, 0, 1); - vertexList.push(...vertices[i]); - vertexList.push(...vertices[(i + 2) % 5]); - } - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -/** - * primitive Rune in the rune of a ribbon - * winding outwards in an anticlockwise spiral - * */ -export const getRibbon: () => Rune = () => { - const vertexList: number[] = []; - const colorList: number[] = []; - - const thetaMax = 30; - const thickness = -1 / thetaMax; - const unit = 0.1; - - const vertices: number[][] = []; - for (let i = 0; i < thetaMax; i += unit) { - vertices.push([ - (i / thetaMax) * Math.cos(i), - (i / thetaMax) * Math.sin(i), - 0, - 1, - ]); - vertices.push([ - Math.abs(Math.cos(i) * thickness) + (i / thetaMax) * Math.cos(i), - Math.abs(Math.sin(i) * thickness) + (i / thetaMax) * Math.sin(i), - 0, - 1, - ]); - } - for (let i = 0; i < vertices.length - 2; i += 1) { - vertexList.push(...vertices[i]); - vertexList.push(...vertices[i + 1]); - vertexList.push(...vertices[i + 2]); - } - - colorList.push(0, 0, 0, 1); - - return Rune.of({ - vertices: new Float32Array(vertexList), - colors: new Float32Array(colorList), - }); -}; - -// ============================================================================= -// Coloring Functions -// ============================================================================= -// black and white not included because they are boring colors -// colorPalette is used in generateFlattenedRuneList to generate a random color -export const colorPalette = [ - '#F44336', - '#E91E63', - '#AA00FF', - '#3F51B5', - '#2196F3', - '#4CAF50', - '#FFEB3B', - '#FF9800', - '#795548', -]; - -export function hexToColor(hex): number[] { - // eslint-disable-next-line prefer-named-capture-group - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/iu.exec(hex); - if (result === null || result.length < 4) { - return [0, 0, 0]; - } - return [ - parseInt(result[1], 16) / 255, - parseInt(result[2], 16) / 255, - parseInt(result[3], 16) / 255, - 1, - ]; -} - -export function addColorFromHex(rune, hex) { - throwIfNotRune('addColorFromHex', rune); - return Rune.of({ - subRunes: [rune], - colors: new Float32Array(hexToColor(hex)), - }); -} +/** + * This file contains the bundle's private functions for runes. + */ +import { Rune } from './rune'; + +// ============================================================================= +// Utility Functions +// ============================================================================= +export function throwIfNotRune(name, ...runes) { + runes.forEach((rune) => { + if (!(rune instanceof Rune)) { + throw Error(`${name} expects a rune as argument.`); + } + }); +} + +// ============================================================================= +// Basic Runes +// ============================================================================= + +/** + * primitive Rune in the rune of a full square + * */ +export const getSquare: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + vertexList.push(-1, 1, 0, 1); + vertexList.push(-1, -1, 0, 1); + vertexList.push(1, -1, 0, 1); + vertexList.push(1, -1, 0, 1); + vertexList.push(-1, 1, 0, 1); + vertexList.push(1, 1, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +export const getBlank: () => Rune = () => Rune.of(); + +/** + * primitive Rune in the rune of a + * smallsquare inside a large square, + * each diagonally split into a + * black and white half + * */ +export const getRcross: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + // lower small triangle + vertexList.push(-0.5, 0.5, 0, 1); + vertexList.push(-0.5, -0.5, 0, 1); + vertexList.push(0.5, -0.5, 0, 1); + + // upper shape, starting from left-top corner + vertexList.push(-1, 1, 0, 1); + vertexList.push(-0.5, 0.5, 0, 1); + vertexList.push(1, 1, 0, 1); + + vertexList.push(-0.5, 0.5, 0, 1); + vertexList.push(1, 1, 0, 1); + vertexList.push(0.5, 0.5, 0, 1); + + vertexList.push(1, 1, 0, 1); + vertexList.push(0.5, 0.5, 0, 1); + vertexList.push(1, -1, 0, 1); + + vertexList.push(0.5, 0.5, 0, 1); + vertexList.push(1, -1, 0, 1); + vertexList.push(0.5, -0.5, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a sail + * */ +export const getSail: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + vertexList.push(0.5, -1, 0, 1); + vertexList.push(0, -1, 0, 1); + vertexList.push(0, 1, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a triangle + * */ +export const getTriangle: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + vertexList.push(1, -1, 0, 1); + vertexList.push(0, -1, 0, 1); + vertexList.push(0, 1, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune with black triangle, + * filling upper right corner + * */ +export const getCorner: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + vertexList.push(1, 0, 0, 1); + vertexList.push(1, 1, 0, 1); + vertexList.push(0, 1, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of two overlapping + * triangles, residing in the upper half + * of + * */ +export const getNova: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + vertexList.push(0, 1, 0, 1); + vertexList.push(-0.5, 0, 0, 1); + vertexList.push(0, 0.5, 0, 1); + + vertexList.push(-0.5, 0, 0, 1); + vertexList.push(0, 0.5, 0, 1); + vertexList.push(1, 0, 0, 1); + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a circle + * */ +export const getCircle: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + const circleDiv = 60; + for (let i = 0; i < circleDiv; i += 1) { + const angle1 = ((2 * Math.PI) / circleDiv) * i; + const angle2 = ((2 * Math.PI) / circleDiv) * (i + 1); + vertexList.push(Math.cos(angle1), Math.sin(angle1), 0, 1); + vertexList.push(Math.cos(angle2), Math.sin(angle2), 0, 1); + vertexList.push(0, 0, 0, 1); + } + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a heart + * */ +export const getHeart: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + const root2 = Math.sqrt(2); + const r = 4 / (2 + 3 * root2); + const scaleX = 1 / (r * (1 + root2 / 2)); + const numPoints = 10; + + // right semi-circle + const rightCenterX = r / root2; + const rightCenterY = 1 - r; + for (let i = 0; i < numPoints; i += 1) { + const angle1 = Math.PI * (-1 / 4 + i / numPoints); + const angle2 = Math.PI * (-1 / 4 + (i + 1) / numPoints); + vertexList.push( + (Math.cos(angle1) * r + rightCenterX) * scaleX, + Math.sin(angle1) * r + rightCenterY, + 0, + 1, + ); + vertexList.push( + (Math.cos(angle2) * r + rightCenterX) * scaleX, + Math.sin(angle2) * r + rightCenterY, + 0, + 1, + ); + vertexList.push(0, -1, 0, 1); + } + // left semi-circle + const leftCenterX = -r / root2; + const leftCenterY = 1 - r; + for (let i = 0; i <= numPoints; i += 1) { + const angle1 = Math.PI * (1 / 4 + i / numPoints); + const angle2 = Math.PI * (1 / 4 + (i + 1) / numPoints); + vertexList.push( + (Math.cos(angle1) * r + leftCenterX) * scaleX, + Math.sin(angle1) * r + leftCenterY, + 0, + 1, + ); + vertexList.push( + (Math.cos(angle2) * r + leftCenterX) * scaleX, + Math.sin(angle2) * r + leftCenterY, + 0, + 1, + ); + vertexList.push(0, -1, 0, 1); + } + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a pentagram + * */ +export const getPentagram: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + const v1 = Math.sin(Math.PI / 10); + const v2 = Math.cos(Math.PI / 10); + + const w1 = Math.sin((3 * Math.PI) / 10); + const w2 = Math.cos((3 * Math.PI) / 10); + + const vertices: number[][] = []; + vertices.push([v2, v1, 0, 1]); + vertices.push([w2, -w1, 0, 1]); + vertices.push([-w2, -w1, 0, 1]); + vertices.push([-v2, v1, 0, 1]); + vertices.push([0, 1, 0, 1]); + + for (let i = 0; i < 5; i += 1) { + vertexList.push(0, 0, 0, 1); + vertexList.push(...vertices[i]); + vertexList.push(...vertices[(i + 2) % 5]); + } + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +/** + * primitive Rune in the rune of a ribbon + * winding outwards in an anticlockwise spiral + * */ +export const getRibbon: () => Rune = () => { + const vertexList: number[] = []; + const colorList: number[] = []; + + const thetaMax = 30; + const thickness = -1 / thetaMax; + const unit = 0.1; + + const vertices: number[][] = []; + for (let i = 0; i < thetaMax; i += unit) { + vertices.push([ + (i / thetaMax) * Math.cos(i), + (i / thetaMax) * Math.sin(i), + 0, + 1, + ]); + vertices.push([ + Math.abs(Math.cos(i) * thickness) + (i / thetaMax) * Math.cos(i), + Math.abs(Math.sin(i) * thickness) + (i / thetaMax) * Math.sin(i), + 0, + 1, + ]); + } + for (let i = 0; i < vertices.length - 2; i += 1) { + vertexList.push(...vertices[i]); + vertexList.push(...vertices[i + 1]); + vertexList.push(...vertices[i + 2]); + } + + colorList.push(0, 0, 0, 1); + + return Rune.of({ + vertices: new Float32Array(vertexList), + colors: new Float32Array(colorList), + }); +}; + +// ============================================================================= +// Coloring Functions +// ============================================================================= +// black and white not included because they are boring colors +// colorPalette is used in generateFlattenedRuneList to generate a random color +export const colorPalette = [ + '#F44336', + '#E91E63', + '#AA00FF', + '#3F51B5', + '#2196F3', + '#4CAF50', + '#FFEB3B', + '#FF9800', + '#795548', +]; + +export function hexToColor(hex): number[] { + // eslint-disable-next-line prefer-named-capture-group + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/iu.exec(hex); + if (result === null || result.length < 4) { + return [0, 0, 0]; + } + return [ + parseInt(result[1], 16) / 255, + parseInt(result[2], 16) / 255, + parseInt(result[3], 16) / 255, + 1, + ]; +} + +export function addColorFromHex(rune, hex) { + throwIfNotRune('addColorFromHex', rune); + return Rune.of({ + subRunes: [rune], + colors: new Float32Array(hexToColor(hex)), + }); +} diff --git a/src/bundles/rune/runes_webgl.ts b/src/bundles/rune/runes_webgl.ts index 28d788538..5a7640697 100644 --- a/src/bundles/rune/runes_webgl.ts +++ b/src/bundles/rune/runes_webgl.ts @@ -1,163 +1,163 @@ -/** - * This file contains the module's private functions that handles various webgl operations. - */ - -export type FrameBufferWithTexture = { - framebuffer: WebGLFramebuffer; - texture: WebGLTexture; -}; - -// The following 2 functions loadShader and initShaderProgram are copied from the curve library, 26 Jul 2021 with no change. This unfortunately violated DIY priciple but I have no choice as those functions are not exported. -/** - * Gets shader based on given shader program code. - * - * @param gl - WebGL's rendering context - * @param type - Constant describing the type of shader to load - * @param source - Source code of the shader - * @returns WebGLShader used to initialize shader program - */ -function loadShader( - gl: WebGLRenderingContext, - type: number, - source: string -): WebGLShader { - const shader = gl.createShader(type); - if (!shader) { - throw new Error('WebGLShader not available.'); - } - gl.shaderSource(shader, source); - gl.compileShader(shader); - const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); - if (!compiled) { - const compilationLog = gl.getShaderInfoLog(shader); - throw Error(`Shader compilation failed: ${compilationLog}`); - } - return shader; -} - -/** - * Initializes the shader program used by WebGL. - * - * @param gl - WebGL's rendering context - * @param vsSource - Vertex shader program code - * @param fsSource - Fragment shader program code - * @returns WebGLProgram used for getting AttribLocation and UniformLocation - */ -export function initShaderProgram( - gl: WebGLRenderingContext, - vsSource: string, - fsSource: string -): WebGLProgram { - const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); - const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); - const shaderProgram = gl.createProgram(); - if (!shaderProgram) { - throw new Error('Unable to initialize the shader program.'); - } - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - return shaderProgram; -} - -/** - * Get a WebGLRenderingContext from Canvas input - * @param canvas WebGLRenderingContext - * @returns - */ -export function getWebGlFromCanvas( - canvas: HTMLCanvasElement -): WebGLRenderingContext { - const gl: WebGLRenderingContext | null = canvas.getContext('webgl'); - if (!gl) { - throw Error('Unable to initialize WebGL.'); - } - gl.clearColor(1.0, 1.0, 1.0, 1.0); // Set clear color to white, fully opaque - gl.enable(gl.DEPTH_TEST); // Enable depth testing - gl.depthFunc(gl.LESS); // Near things obscure far things (this is default setting can omit) - // eslint-disable-next-line no-bitwise - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the viewport - return gl; -} - -/** - * creates a framebuffer - * @param gl WebGLRenderingContext - * @returns FrameBufferWithTexture - */ -export function initFramebufferObject( - gl: WebGLRenderingContext -): FrameBufferWithTexture { - // create a framebuffer object - const framebuffer = gl.createFramebuffer(); - if (!framebuffer) { - throw Error('Failed to create frame buffer object'); - } - - // create a texture object and set its size and parameters - const texture = gl.createTexture(); - if (!texture) { - throw Error('Failed to create texture object'); - } - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.drawingBufferWidth, - gl.drawingBufferHeight, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - null - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - - // create a renderbuffer for depth buffer - const depthBuffer = gl.createRenderbuffer(); - if (!depthBuffer) { - throw Error('Failed to create renderbuffer object'); - } - - // bind renderbuffer object to target and set size - gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); - gl.renderbufferStorage( - gl.RENDERBUFFER, - gl.DEPTH_COMPONENT16, - gl.drawingBufferWidth, - gl.drawingBufferHeight - ); - - // set the texture object to the framebuffer object - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // bind to target - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - texture, - 0 - ); - // set the renderbuffer object to the framebuffer object - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - gl.DEPTH_ATTACHMENT, - gl.RENDERBUFFER, - depthBuffer - ); - - // check whether the framebuffer is configured correctly - const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); - if (gl.FRAMEBUFFER_COMPLETE !== e) { - throw Error(`Frame buffer object is incomplete:${e.toString()}`); - } - - // Unbind the buffer object - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - gl.bindTexture(gl.TEXTURE_2D, null); - gl.bindRenderbuffer(gl.RENDERBUFFER, null); - - return { - framebuffer, - texture, - }; -} +/** + * This file contains the module's private functions that handles various webgl operations. + */ + +export type FrameBufferWithTexture = { + framebuffer: WebGLFramebuffer; + texture: WebGLTexture; +}; + +// The following 2 functions loadShader and initShaderProgram are copied from the curve library, 26 Jul 2021 with no change. This unfortunately violated DIY priciple but I have no choice as those functions are not exported. +/** + * Gets shader based on given shader program code. + * + * @param gl - WebGL's rendering context + * @param type - Constant describing the type of shader to load + * @param source - Source code of the shader + * @returns WebGLShader used to initialize shader program + */ +function loadShader( + gl: WebGLRenderingContext, + type: number, + source: string, +): WebGLShader { + const shader = gl.createShader(type); + if (!shader) { + throw new Error('WebGLShader not available.'); + } + gl.shaderSource(shader, source); + gl.compileShader(shader); + const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + const compilationLog = gl.getShaderInfoLog(shader); + throw Error(`Shader compilation failed: ${compilationLog}`); + } + return shader; +} + +/** + * Initializes the shader program used by WebGL. + * + * @param gl - WebGL's rendering context + * @param vsSource - Vertex shader program code + * @param fsSource - Fragment shader program code + * @returns WebGLProgram used for getting AttribLocation and UniformLocation + */ +export function initShaderProgram( + gl: WebGLRenderingContext, + vsSource: string, + fsSource: string, +): WebGLProgram { + const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource); + const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource); + const shaderProgram = gl.createProgram(); + if (!shaderProgram) { + throw new Error('Unable to initialize the shader program.'); + } + gl.attachShader(shaderProgram, vertexShader); + gl.attachShader(shaderProgram, fragmentShader); + gl.linkProgram(shaderProgram); + return shaderProgram; +} + +/** + * Get a WebGLRenderingContext from Canvas input + * @param canvas WebGLRenderingContext + * @returns + */ +export function getWebGlFromCanvas( + canvas: HTMLCanvasElement, +): WebGLRenderingContext { + const gl: WebGLRenderingContext | null = canvas.getContext('webgl'); + if (!gl) { + throw Error('Unable to initialize WebGL.'); + } + gl.clearColor(1.0, 1.0, 1.0, 1.0); // Set clear color to white, fully opaque + gl.enable(gl.DEPTH_TEST); // Enable depth testing + gl.depthFunc(gl.LESS); // Near things obscure far things (this is default setting can omit) + // eslint-disable-next-line no-bitwise + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Clear the viewport + return gl; +} + +/** + * creates a framebuffer + * @param gl WebGLRenderingContext + * @returns FrameBufferWithTexture + */ +export function initFramebufferObject( + gl: WebGLRenderingContext, +): FrameBufferWithTexture { + // create a framebuffer object + const framebuffer = gl.createFramebuffer(); + if (!framebuffer) { + throw Error('Failed to create frame buffer object'); + } + + // create a texture object and set its size and parameters + const texture = gl.createTexture(); + if (!texture) { + throw Error('Failed to create texture object'); + } + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null, + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + + // create a renderbuffer for depth buffer + const depthBuffer = gl.createRenderbuffer(); + if (!depthBuffer) { + throw Error('Failed to create renderbuffer object'); + } + + // bind renderbuffer object to target and set size + gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); + gl.renderbufferStorage( + gl.RENDERBUFFER, + gl.DEPTH_COMPONENT16, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + ); + + // set the texture object to the framebuffer object + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); // bind to target + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + texture, + 0, + ); + // set the renderbuffer object to the framebuffer object + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, + depthBuffer, + ); + + // check whether the framebuffer is configured correctly + const e = gl.checkFramebufferStatus(gl.FRAMEBUFFER); + if (gl.FRAMEBUFFER_COMPLETE !== e) { + throw Error(`Frame buffer object is incomplete:${e.toString()}`); + } + + // Unbind the buffer object + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + + return { + framebuffer, + texture, + }; +} diff --git a/src/bundles/scrabble/functions.ts b/src/bundles/scrabble/functions.ts index 03a3c5c5d..4346f845a 100644 --- a/src/bundles/scrabble/functions.ts +++ b/src/bundles/scrabble/functions.ts @@ -67612,7 +67612,7 @@ export const scrabble_list = current_list; export function charAt(s: string, i: number): any { const result = s.charAt(i); - return result === `` ? undefined : result; + return result === '' ? undefined : result; } export function arrayLength(x: any): number { diff --git a/src/bundles/sound/functions.ts b/src/bundles/sound/functions.ts index b46cdecc7..1d7fb2b01 100644 --- a/src/bundles/sound/functions.ts +++ b/src/bundles/sound/functions.ts @@ -1,942 +1,940 @@ -/** - * The sounds library provides functions for constructing and playing sounds. - * - * A wave is a function that takes in a number `t` and returns - * a number representing the amplitude at time `t`. - * The amplitude should fall within the range of [-1, 1]. - * - * A Sound is a pair(wave, duration) where duration is the length of the sound in seconds. - * The constructor make_sound and accessors get_wave and get_duration are provided. - * - * Sound Discipline: - * For all sounds, the wave function applied to and time `t` beyond its duration returns 0, that is: - * `(get_wave(sound))(get_duration(sound) + x) === 0` for any x >= 0. - * - * Two functions which combine Sounds, `consecutively` and `simultaneously` are given. - * Additionally, we provide sound transformation functions `adsr` and `phase_mod` - * which take in a Sound and return a Sound. - * - * Finally, the provided `play` function takes in a Sound and plays it using your - * computer's sound system. - * - * @module sound - * @author Koh Shang Hui - * @author Samyukta Sounderraman - */ - -/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-use-before-define, @typescript-eslint/no-unused-vars, new-cap */ -import { - Wave, - Sound, - SoundProducer, - SoundTransformer, - List, - AudioPlayed, -} from './types'; -import { - pair, - head, - tail, - list, - length, - is_null, - is_pair, - accumulate, -} from './list'; -import { RIFFWAVE } from './riffwave'; - -// Global Constants and Variables -let audioElement: HTMLAudioElement; -const FS: number = 44100; // Output sample rate -const fourier_expansion_level: number = 5; // fourier expansion level - -/** @hidden */ -export const audioPlayed: AudioPlayed[] = []; - -// Singular audio context for all playback functions -let audioplayer: AudioContext; - -// Track if a sound is currently playing -let isPlaying: boolean; - -// Instantiates new audio context -function init_audioCtx(): void { - audioplayer = new window.AudioContext(); - // audioplayer = new (window.AudioContext || window.webkitAudioContext)(); -} - -// linear decay from 1 to 0 over decay_period -function linear_decay(decay_period: number): (t: number) => number { - return (t) => { - if (t > decay_period || t < 0) { - return 0; - } - return 1 - t / decay_period; - }; -} - -// // --------------------------------------------- -// // Microphone Functionality -// // --------------------------------------------- - -// permission initially undefined -// set to true by granting microphone permission -// set to false by denying microphone permission -let permission: boolean | undefined; - -let recorded_sound: Sound | undefined; - -// check_permission is called whenever we try -// to record a sound -function check_permission() { - if (permission === undefined) { - throw new Error( - `Call init_record(); to obtain permission to use microphone` - ); - } else if (permission === false) { - throw new Error(`Permission has been denied.\n - Re-start browser and call init_record();\n - to obtain permission to use microphone.`); - } // (permission === true): do nothing -} - -let globalStream: any; - -function rememberStream(stream: any) { - permission = true; - globalStream = stream; -} - -function setPermissionToFalse() { - permission = false; -} - -function start_recording(mediaRecorder: MediaRecorder) { - const data: any[] = []; - // eslint-disable-next-line no-param-reassign - mediaRecorder.ondataavailable = (e) => e.data.size && data.push(e.data); - mediaRecorder.start(); - // eslint-disable-next-line no-param-reassign - mediaRecorder.onstop = () => process(data); -} - -// duration of recording signal in milliseconds -const recording_signal_ms = 100; - -// duration of pause after "run" before recording signal is played -const pre_recording_signal_pause_ms = 200; - -function play_recording_signal() { - play(sine_sound(1200, recording_signal_ms / 1000)); -} - -// eslint-disable-next-line @typescript-eslint/no-shadow -function process(data) { - const audioContext = new AudioContext(); - const blob = new Blob(data); - - convertToArrayBuffer(blob) - .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer)) - .then(save); -} - -// Converts input microphone sound (blob) into array format. -function convertToArrayBuffer(blob: Blob): Promise { - const url = URL.createObjectURL(blob); - return fetch(url).then((response) => response.arrayBuffer()); -} - -function save(audioBuffer: AudioBuffer) { - const array = audioBuffer.getChannelData(0); - const duration = array.length / FS; - recorded_sound = make_sound((t) => { - const index = t * FS; - const lowerIndex = Math.floor(index); - const upperIndex = lowerIndex + 1; - const ratio = index - lowerIndex; - const upper = array[upperIndex] ? array[upperIndex] : 0; - const lower = array[lowerIndex] ? array[lowerIndex] : 0; - return lower * (1 - ratio) + upper * ratio; - }, duration); -} - -/** - * Initialize recording by obtaining permission - * to use the default device microphone - * - * @returns string "obtaining recording permission" - */ -export function init_record(): string { - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then(rememberStream, setPermissionToFalse); - return 'obtaining recording permission'; -} - -/** - * takes a buffer duration (in seconds) as argument, and - * returns a nullary stop function stop. A call - * stop() returns a sound promise: a nullary function - * that returns a sound. Example:
    init_record();
    - * const stop = record(0.5);
    - * // record after 0.5 seconds. Then in next query:
    - * const promise = stop();
    - * // In next query, you can play the promised sound, by
    - * // applying the promise:
    - * play(promise());
    - * @param buffer - pause before recording, in seconds - * @returns nullary stop function; - * stop() stops the recording and - * returns a sound promise: a nullary function that returns the recorded sound - */ -export function record(buffer: number): () => () => Sound { - check_permission(); - const mediaRecorder = new MediaRecorder(globalStream); - setTimeout(() => { - play_recording_signal(); - start_recording(mediaRecorder); - }, recording_signal_ms + buffer * 1000); - return () => { - mediaRecorder.stop(); - play_recording_signal(); - return () => { - if (recorded_sound === undefined) { - throw new Error('recording still being processed'); - } else { - return recorded_sound; - } - }; - }; -} - -/** - * Records a sound of given duration in seconds, after - * a buffer also in seconds, and - * returns a sound promise: a nullary function - * that returns a sound. Example:
    init_record();
    - * const promise = record_for(2, 0.5);
    - * // In next query, you can play the promised sound, by
    - * // applying the promise:
    - * play(promise());
    - * @param duration duration in seconds - * @param buffer pause before recording, in seconds - * @return promise: nullary function which returns recorded sound - */ -export function record_for(duration: number, buffer: number): () => Sound { - recorded_sound = undefined; - const recording_ms = duration * 1000; - const pre_recording_pause_ms = buffer * 1000; - check_permission(); - const mediaRecorder = new MediaRecorder(globalStream); - - // order of events for record_for: - // pre-recording-signal pause | recording signal | - // pre-recording pause | recording | recording signal - - setTimeout(() => { - play_recording_signal(); - setTimeout(() => { - start_recording(mediaRecorder); - setTimeout(() => { - mediaRecorder.stop(); - play_recording_signal(); - }, recording_ms); - }, recording_signal_ms + pre_recording_pause_ms); - }, pre_recording_signal_pause_ms); - - return () => { - if (recorded_sound === undefined) { - throw new Error('recording still being processed'); - } else { - return recorded_sound; - } - }; -} - -// ============================================================================= -// Module's Exposed Functions -// -// This file only includes the implementation and documentation of exposed -// functions of the module. For private functions dealing with the browser's -// graphics library context, see './webGL_curves.ts'. -// ============================================================================= - -// Core functions - -/** - * Makes a Sound with given wave function and duration. - * The wave function is a function: number -> number - * that takes in a non-negative input time and returns an amplitude - * between -1 and 1. - * - * @param wave wave function of the sound - * @param duration duration of the sound - * @return with wave as wave function and duration as duration - * @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); - */ -export function make_sound(wave: Wave, duration: number): Sound { - return pair((t: number) => (t >= duration ? 0 : wave(t)), duration); -} - -/** - * Accesses the wave function of a given Sound. - * - * @param sound given Sound - * @return the wave function of the Sound - * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) - */ -export function get_wave(sound: Sound): Wave { - return head(sound); -} - -/** - * Accesses the duration of a given Sound. - * - * @param sound given Sound - * @return the duration of the Sound - * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 - */ -export function get_duration(sound: Sound): number { - return tail(sound); -} - -/** - * Checks if the argument is a Sound - * - * @param x input to be checked - * @return true if x is a Sound, false otherwise - * @example is_sound(make_sound(t => 0, 2)); // Returns true - */ -export function is_sound(x: any): boolean { - return ( - is_pair(x) && - typeof get_wave(x) === 'function' && - typeof get_duration(x) === 'number' - ); -} - -/** - * Plays the given Wave using the computer’s sound device, for the duration - * given in seconds. - * The sound is only played if no other sounds are currently being played. - * - * @param wave the wave function to play, starting at 0 - * @return the given sound - * @example play_wave(t => math_sin(t * 3000), 5); - */ -export function play_wave(wave: Wave, duration: number): AudioPlayed { - return play(make_sound(wave, duration)); -} - -/** - * Plays the given Sound using the computer’s sound device. - * The sound is only played if no other sounds are currently being played. - * - * @param sound the sound to play - * @return the given sound - * @example play(sine_sound(440, 5)); - */ -export function play(sound: Sound): AudioPlayed { - // Type-check sound - if (!is_sound(sound)) { - throw new Error(`play is expecting sound, but encountered ${sound}`); - // If a sound is already playing, terminate execution. - } else if (isPlaying) { - throw new Error('play: audio system still playing previous sound'); - } else if (get_duration(sound) < 0) { - throw new Error('play: duration of sound is negative'); - } else { - // Instantiate audio context if it has not been instantiated. - if (!audioplayer) { - init_audioCtx(); - } - - // Create mono buffer - const channel: number[] = []; - const len = Math.ceil(FS * get_duration(sound)); - - let temp: number; - let prev_value = 0; - - const wave = get_wave(sound); - for (let i = 0; i < len; i += 1) { - temp = wave(i / FS); - // clip amplitude - // channel[i] = temp > 1 ? 1 : temp < -1 ? -1 : temp; - if (temp > 1) { - channel[i] = 1; - } else if (temp < -1) { - channel[i] = -1; - } else { - channel[i] = temp; - } - - // smoothen out sudden cut-outs - if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { - channel[i] = prev_value * 0.999; - } - - prev_value = channel[i]; - } - - // quantize - for (let i = 0; i < channel.length; i += 1) { - channel[i] = Math.floor(channel[i] * 32767.999); - } - - const riffwave = new RIFFWAVE([]); - riffwave.header.sampleRate = FS; - riffwave.header.numChannels = 1; - riffwave.header.bitsPerSample = 16; - riffwave.Make(channel); - - /* - const audio = new Audio(riffwave.dataURI); - const source2 = audioplayer.createMediaElementSource(audio); - source2.connect(audioplayer.destination); - - // Connect data to output destination - isPlaying = true; - audio.play(); - audio.onended = () => { - source2.disconnect(audioplayer.destination); - isPlaying = false; - }; */ - - const soundToPlay = { - toReplString: () => ``, - dataUri: riffwave.dataURI, - }; - audioPlayed.push(soundToPlay); - return soundToPlay; - } -} - -/** - * Plays the given Sound using the computer’s sound device - * on top of any sounds that are currently playing. - * - * @param sound the sound to play - * @example play_concurrently(sine_sound(440, 5)); - */ -export function play_concurrently(sound: Sound): void { - // Type-check sound - if (!is_sound(sound)) { - throw new Error( - `play_concurrently is expecting sound, but encountered ${sound}` - ); - } else if (get_duration(sound) <= 0) { - // Do nothing - } else { - // Instantiate audio context if it has not been instantiated. - if (!audioplayer) { - init_audioCtx(); - } - - // Create mono buffer - const theBuffer = audioplayer.createBuffer( - 1, - Math.ceil(FS * get_duration(sound)), - FS - ); - const channel = theBuffer.getChannelData(0); - - let temp: number; - let prev_value = 0; - - const wave = get_wave(sound); - for (let i = 0; i < channel.length; i += 1) { - temp = wave(i / FS); - // clip amplitude - if (temp > 1) { - channel[i] = 1; - } else if (temp < -1) { - channel[i] = -1; - } else { - channel[i] = temp; - } - - // smoothen out sudden cut-outs - if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { - channel[i] = prev_value * 0.999; - } - - prev_value = channel[i]; - } - - // Connect data to output destination - const source = audioplayer.createBufferSource(); - source.buffer = theBuffer; - source.connect(audioplayer.destination); - isPlaying = true; - source.start(); - source.onended = () => { - source.disconnect(audioplayer.destination); - isPlaying = false; - }; - } -} - -/** - * Stops all currently playing sounds. - */ -export function stop(): void { - audioplayer.close(); - isPlaying = false; -} - -// Primitive sounds - -/** - * Makes a noise sound with given duration - * - * @param duration the duration of the noise sound - * @return resulting noise sound - * @example noise_sound(5); - */ -export function noise_sound(duration: number): Sound { - return make_sound((t) => Math.random() * 2 - 1, duration); -} - -/** - * Makes a silence sound with given duration - * - * @param duration the duration of the silence sound - * @return resulting silence sound - * @example silence_sound(5); - */ -export function silence_sound(duration: number): Sound { - return make_sound((t) => 0, duration); -} - -/** - * Makes a sine wave sound with given frequency and duration - * - * @param freq the frequency of the sine wave sound - * @param duration the duration of the sine wave sound - * @return resulting sine wave sound - * @example sine_sound(440, 5); - */ -export function sine_sound(freq: number, duration: number): Sound { - return make_sound((t) => Math.sin(2 * Math.PI * t * freq), duration); -} - -/** - * Makes a square wave sound with given frequency and duration - * - * @param freq the frequency of the square wave sound - * @param duration the duration of the square wave sound - * @return resulting square wave sound - * @example square_sound(440, 5); - */ -export function square_sound(f: number, duration: number): Sound { - function fourier_expansion_square(t: number) { - let answer = 0; - for (let i = 1; i <= fourier_expansion_level; i += 1) { - answer += Math.sin(2 * Math.PI * (2 * i - 1) * f * t) / (2 * i - 1); - } - return answer; - } - return make_sound( - (t) => (4 / Math.PI) * fourier_expansion_square(t), - duration - ); -} - -/** - * Makes a triangle wave sound with given frequency and duration - * - * @param freq the frequency of the triangle wave sound - * @param duration the duration of the triangle wave sound - * @return resulting triangle wave sound - * @example triangle_sound(440, 5); - */ -export function triangle_sound(freq: number, duration: number): Sound { - function fourier_expansion_triangle(t: number) { - let answer = 0; - for (let i = 0; i < fourier_expansion_level; i += 1) { - answer += - ((-1) ** i * Math.sin((2 * i + 1) * t * freq * Math.PI * 2)) / - (2 * i + 1) ** 2; - } - return answer; - } - return make_sound( - (t) => (8 / Math.PI / Math.PI) * fourier_expansion_triangle(t), - duration - ); -} - -/** - * Makes a sawtooth wave sound with given frequency and duration - * - * @param freq the frequency of the sawtooth wave sound - * @param duration the duration of the sawtooth wave sound - * @return resulting sawtooth wave sound - * @example sawtooth_sound(440, 5); - */ -export function sawtooth_sound(freq: number, duration: number): Sound { - function fourier_expansion_sawtooth(t: number) { - let answer = 0; - for (let i = 1; i <= fourier_expansion_level; i += 1) { - answer += Math.sin(2 * Math.PI * i * freq * t) / i; - } - return answer; - } - return make_sound( - (t) => 1 / 2 - (1 / Math.PI) * fourier_expansion_sawtooth(t), - duration - ); -} - -// Composition Operators - -/** - * Makes a new Sound by combining the sounds in a given list - * where the second sound is appended to the end of the first sound, - * the third sound is appended to the end of the second sound, and - * so on. The effect is that the sounds in the list are joined end-to-end - * - * @param list_of_sounds given list of sounds - * @return the combined Sound - * @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); - */ -export function consecutively(list_of_sounds: List): Sound { - function consec_two(ss1: Sound, ss2: Sound) { - const wave1 = get_wave(ss1); - const wave2 = get_wave(ss2); - const dur1 = get_duration(ss1); - const dur2 = get_duration(ss2); - const new_wave = (t: number) => (t < dur1 ? wave1(t) : wave2(t - dur1)); - return make_sound(new_wave, dur1 + dur2); - } - return accumulate(consec_two, silence_sound(0), list_of_sounds); -} - -/** - * Makes a new Sound by combining the sounds in a given list - * where all the sounds are overlapped on top of each other. - * - * @param list_of_sounds given list of sounds - * @return the combined Sound - * @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))) - */ -export function simultaneously(list_of_sounds: List): Sound { - function simul_two(ss1: Sound, ss2: Sound) { - const wave1 = get_wave(ss1); - const wave2 = get_wave(ss2); - const dur1 = get_duration(ss1); - const dur2 = get_duration(ss2); - // new_wave assumes sound discipline (ie, wave(t) = 0 after t > dur) - const new_wave = (t: number) => wave1(t) + wave2(t); - // new_dur is higher of the two dur - const new_dur = dur1 < dur2 ? dur2 : dur1; - return make_sound(new_wave, new_dur); - } - - const mushed_sounds = accumulate(simul_two, silence_sound(0), list_of_sounds); - const normalised_wave = (t: number) => - head(mushed_sounds)(t) / length(list_of_sounds); - const highest_duration = tail(mushed_sounds); - return make_sound(normalised_wave, highest_duration); -} - -/** - * Returns an envelope: a function from Sound to Sound. - * When the adsr envelope is applied to a Sound, it returns - * a new Sound with its amplitude modified according to parameters - * The relative amplitude increases from 0 to 1 linearly over the - * attack proportion, then decreases from 1 to sustain level over the - * decay proportion, and remains at that level until the release - * proportion when it decays back to 0. - * @param attack_ratio proportion of Sound in attack phase - * @param decay_ratio proportion of Sound decay phase - * @param sustain_level sustain level between 0 and 1 - * @param release_ratio proportion of Sound in release phase - * @return Envelope a function from Sound to Sound - * @example adsr(0.2, 0.3, 0.3, 0.1)(sound); - */ -export function adsr( - attack_ratio: number, - decay_ratio: number, - sustain_level: number, - release_ratio: number -): SoundTransformer { - return (sound) => { - const wave = get_wave(sound); - const duration = get_duration(sound); - const attack_time = duration * attack_ratio; - const decay_time = duration * decay_ratio; - const release_time = duration * release_ratio; - return make_sound((x) => { - if (x < attack_time) { - return wave(x) * (x / attack_time); - } - if (x < attack_time + decay_time) { - return ( - ((1 - sustain_level) * linear_decay(decay_time)(x - attack_time) + - sustain_level) * - wave(x) - ); - } - if (x < duration - release_time) { - return wave(x) * sustain_level; - } - return ( - wave(x) * - sustain_level * - linear_decay(release_time)(x - (duration - release_time)) - ); - }, duration); - }; -} - -/** - * Returns a Sound that results from applying a list of envelopes - * to a given wave form. The wave form is a Sound generator that - * takes a frequency and a duration as arguments and produces a - * Sound with the given frequency and duration. Each envelope is - * applied to a harmonic: the first harmonic has the given frequency, - * the second has twice the frequency, the third three times the - * frequency etc. The harmonics are then layered simultaneously to - * produce the resulting Sound. - * @param waveform function from pair(frequency, duration) to Sound - * @param base_frequency frequency of the first harmonic - * @param duration duration of the produced Sound, in seconds - * @param envelopes – list of envelopes, which are functions from Sound to Sound - * @return Sound resulting Sound - * @example stacking_adsr(sine_sound, 300, 5, list(adsr(0.1, 0.3, 0.2, 0.5), adsr(0.2, 0.5, 0.6, 0.1), adsr(0.3, 0.1, 0.7, 0.3))); - */ -export function stacking_adsr( - waveform: SoundProducer, - base_frequency: number, - duration: number, - envelopes: List -): Sound { - function zip(lst: List, n: number) { - if (is_null(lst)) { - return lst; - } - return pair(pair(n, head(lst)), zip(tail(lst), n + 1)); - } - - return simultaneously( - accumulate( - (x: any, y: any) => - pair(tail(x)(waveform(base_frequency * head(x), duration)), y), - null, - zip(envelopes, 1) - ) - ); -} - -/** - * Returns a SoundTransformer which uses its argument - * to modulate the phase of a (carrier) sine wave - * of given frequency and duration with a given Sound. - * Modulating with a low frequency Sound results in a vibrato effect. - * Modulating with a Sound with frequencies comparable to - * the sine wave frequency results in more complex wave forms. - * - * @param freq the frequency of the sine wave to be modulated - * @param duration the duration of the output soud - * @param amount the amount of modulation to apply to the carrier sine wave - * @return function which takes in a Sound and returns a Sound - * @example phase_mod(440, 5, 1)(sine_sound(220, 5)); - */ -export function phase_mod( - freq: number, - duration: number, - amount: number -): SoundTransformer { - return (modulator: Sound) => - make_sound( - (t) => Math.sin(2 * Math.PI * t * freq + amount * get_wave(modulator)(t)), - duration - ); -} - -// MIDI conversion functions - -/** - * Converts a letter name to its corresponding MIDI note. - * The letter name is represented in standard pitch notation. - * Examples are "A5", "Db3", "C#7". - * Refer to this mapping from - * letter name to midi notes. - * - * @param letter_name given letter name - * @return the corresponding midi note - * @example letter_name_to_midi_note("C4"); // Returns 60 - */ -export function letter_name_to_midi_note(note: string): number { - let res = 12; // C0 is midi note 12 - const n = note[0].toUpperCase(); - switch (n) { - case 'D': - res += 2; - break; - - case 'E': - res += 4; - break; - - case 'F': - res += 5; - break; - - case 'G': - res += 7; - break; - - case 'A': - res += 9; - break; - - case 'B': - res += 11; - break; - - default: - break; - } - - if (note.length === 2) { - res += parseInt(note[1]) * 12; - } else if (note.length === 3) { - switch (note[1]) { - case '#': - res += 1; - break; - - case 'b': - res -= 1; - break; - - default: - break; - } - res += parseInt(note[2]) * 12; - } - return res; -} - -/** - * Converts a MIDI note to its corresponding frequency. - * - * @param note given MIDI note - * @return the frequency of the MIDI note - * @example midi_note_to_frequency(69); // Returns 440 - */ -export function midi_note_to_frequency(note: number): number { - // A4 = 440Hz = midi note 69 - return 440 * 2 ** ((note - 69) / 12); -} - -/** - * Converts a letter name to its corresponding frequency. - * - * @param letter_name given letter name - * @return the corresponding frequency - * @example letter_name_to_frequency("A4"); // Returns 440 - */ -export function letter_name_to_frequency(note: string): number { - return midi_note_to_frequency(letter_name_to_midi_note(note)); -} - -// Instruments - -/** - * returns a Sound reminiscent of a bell, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting bell Sound with given pitch and duration - * @example bell(40, 1); - */ -export function bell(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list( - adsr(0, 0.6, 0, 0.05), - adsr(0, 0.6618, 0, 0.05), - adsr(0, 0.7618, 0, 0.05), - adsr(0, 0.9071, 0, 0.05) - ) - ); -} - -/** - * returns a Sound reminiscent of a cello, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting cello Sound with given pitch and duration - * @example cello(36, 5); - */ -export function cello(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0.05, 0, 1, 0.1), adsr(0.05, 0, 1, 0.15), adsr(0, 0, 0.2, 0.15)) - ); -} - -/** - * returns a Sound reminiscent of a piano, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting piano Sound with given pitch and duration - * @example piano(48, 5); - */ -export function piano(note: number, duration: number): Sound { - return stacking_adsr( - triangle_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0, 0.515, 0, 0.05), adsr(0, 0.32, 0, 0.05), adsr(0, 0.2, 0, 0.05)) - ); -} - -/** - * returns a Sound reminiscent of a trombone, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting trombone Sound with given pitch and duration - * @example trombone(60, 2); - */ -export function trombone(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0.2, 0, 1, 0.1), adsr(0.3236, 0.6, 0, 0.1)) - ); -} - -/** - * returns a Sound reminiscent of a violin, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting violin Sound with given pitch and duration - * @example violin(53, 4); - */ -export function violin(note: number, duration: number): Sound { - return stacking_adsr( - sawtooth_sound, - midi_note_to_frequency(note), - duration, - list( - adsr(0.35, 0, 1, 0.15), - adsr(0.35, 0, 1, 0.15), - adsr(0.45, 0, 1, 0.15), - adsr(0.45, 0, 1, 0.15) - ) - ); -} +/** + * The sounds library provides functions for constructing and playing sounds. + * + * A wave is a function that takes in a number `t` and returns + * a number representing the amplitude at time `t`. + * The amplitude should fall within the range of [-1, 1]. + * + * A Sound is a pair(wave, duration) where duration is the length of the sound in seconds. + * The constructor make_sound and accessors get_wave and get_duration are provided. + * + * Sound Discipline: + * For all sounds, the wave function applied to and time `t` beyond its duration returns 0, that is: + * `(get_wave(sound))(get_duration(sound) + x) === 0` for any x >= 0. + * + * Two functions which combine Sounds, `consecutively` and `simultaneously` are given. + * Additionally, we provide sound transformation functions `adsr` and `phase_mod` + * which take in a Sound and return a Sound. + * + * Finally, the provided `play` function takes in a Sound and plays it using your + * computer's sound system. + * + * @module sound + * @author Koh Shang Hui + * @author Samyukta Sounderraman + */ + +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-use-before-define, @typescript-eslint/no-unused-vars, new-cap */ +import { + Wave, + Sound, + SoundProducer, + SoundTransformer, + List, + AudioPlayed, +} from './types'; +import { + pair, + head, + tail, + list, + length, + is_null, + is_pair, + accumulate, +} from './list'; +import { RIFFWAVE } from './riffwave'; + +// Global Constants and Variables +let audioElement: HTMLAudioElement; +const FS: number = 44100; // Output sample rate +const fourier_expansion_level: number = 5; // fourier expansion level + +/** @hidden */ +export const audioPlayed: AudioPlayed[] = []; + +// Singular audio context for all playback functions +let audioplayer: AudioContext; + +// Track if a sound is currently playing +let isPlaying: boolean; + +// Instantiates new audio context +function init_audioCtx(): void { + audioplayer = new window.AudioContext(); + // audioplayer = new (window.AudioContext || window.webkitAudioContext)(); +} + +// linear decay from 1 to 0 over decay_period +function linear_decay(decay_period: number): (t: number) => number { + return (t) => { + if (t > decay_period || t < 0) { + return 0; + } + return 1 - t / decay_period; + }; +} + +// // --------------------------------------------- +// // Microphone Functionality +// // --------------------------------------------- + +// permission initially undefined +// set to true by granting microphone permission +// set to false by denying microphone permission +let permission: boolean | undefined; + +let recorded_sound: Sound | undefined; + +// check_permission is called whenever we try +// to record a sound +function check_permission() { + if (permission === undefined) { + throw new Error( + 'Call init_record(); to obtain permission to use microphone', + ); + } else if (permission === false) { + throw new Error(`Permission has been denied.\n + Re-start browser and call init_record();\n + to obtain permission to use microphone.`); + } // (permission === true): do nothing +} + +let globalStream: any; + +function rememberStream(stream: any) { + permission = true; + globalStream = stream; +} + +function setPermissionToFalse() { + permission = false; +} + +function start_recording(mediaRecorder: MediaRecorder) { + const data: any[] = []; + // eslint-disable-next-line no-param-reassign + mediaRecorder.ondataavailable = (e) => e.data.size && data.push(e.data); + mediaRecorder.start(); + // eslint-disable-next-line no-param-reassign + mediaRecorder.onstop = () => process(data); +} + +// duration of recording signal in milliseconds +const recording_signal_ms = 100; + +// duration of pause after "run" before recording signal is played +const pre_recording_signal_pause_ms = 200; + +function play_recording_signal() { + play(sine_sound(1200, recording_signal_ms / 1000)); +} + +// eslint-disable-next-line @typescript-eslint/no-shadow +function process(data) { + const audioContext = new AudioContext(); + const blob = new Blob(data); + + convertToArrayBuffer(blob) + .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer)) + .then(save); +} + +// Converts input microphone sound (blob) into array format. +function convertToArrayBuffer(blob: Blob): Promise { + const url = URL.createObjectURL(blob); + return fetch(url) + .then((response) => response.arrayBuffer()); +} + +function save(audioBuffer: AudioBuffer) { + const array = audioBuffer.getChannelData(0); + const duration = array.length / FS; + recorded_sound = make_sound((t) => { + const index = t * FS; + const lowerIndex = Math.floor(index); + const upperIndex = lowerIndex + 1; + const ratio = index - lowerIndex; + const upper = array[upperIndex] ? array[upperIndex] : 0; + const lower = array[lowerIndex] ? array[lowerIndex] : 0; + return lower * (1 - ratio) + upper * ratio; + }, duration); +} + +/** + * Initialize recording by obtaining permission + * to use the default device microphone + * + * @returns string "obtaining recording permission" + */ +export function init_record(): string { + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(rememberStream, setPermissionToFalse); + return 'obtaining recording permission'; +} + +/** + * takes a buffer duration (in seconds) as argument, and + * returns a nullary stop function stop. A call + * stop() returns a sound promise: a nullary function + * that returns a sound. Example:
    init_record();
    + * const stop = record(0.5);
    + * // record after 0.5 seconds. Then in next query:
    + * const promise = stop();
    + * // In next query, you can play the promised sound, by
    + * // applying the promise:
    + * play(promise());
    + * @param buffer - pause before recording, in seconds + * @returns nullary stop function; + * stop() stops the recording and + * returns a sound promise: a nullary function that returns the recorded sound + */ +export function record(buffer: number): () => () => Sound { + check_permission(); + const mediaRecorder = new MediaRecorder(globalStream); + setTimeout(() => { + play_recording_signal(); + start_recording(mediaRecorder); + }, recording_signal_ms + buffer * 1000); + return () => { + mediaRecorder.stop(); + play_recording_signal(); + return () => { + if (recorded_sound === undefined) { + throw new Error('recording still being processed'); + } else { + return recorded_sound; + } + }; + }; +} + +/** + * Records a sound of given duration in seconds, after + * a buffer also in seconds, and + * returns a sound promise: a nullary function + * that returns a sound. Example:
    init_record();
    + * const promise = record_for(2, 0.5);
    + * // In next query, you can play the promised sound, by
    + * // applying the promise:
    + * play(promise());
    + * @param duration duration in seconds + * @param buffer pause before recording, in seconds + * @return promise: nullary function which returns recorded sound + */ +export function record_for(duration: number, buffer: number): () => Sound { + recorded_sound = undefined; + const recording_ms = duration * 1000; + const pre_recording_pause_ms = buffer * 1000; + check_permission(); + const mediaRecorder = new MediaRecorder(globalStream); + + // order of events for record_for: + // pre-recording-signal pause | recording signal | + // pre-recording pause | recording | recording signal + + setTimeout(() => { + play_recording_signal(); + setTimeout(() => { + start_recording(mediaRecorder); + setTimeout(() => { + mediaRecorder.stop(); + play_recording_signal(); + }, recording_ms); + }, recording_signal_ms + pre_recording_pause_ms); + }, pre_recording_signal_pause_ms); + + return () => { + if (recorded_sound === undefined) { + throw new Error('recording still being processed'); + } else { + return recorded_sound; + } + }; +} + +// ============================================================================= +// Module's Exposed Functions +// +// This file only includes the implementation and documentation of exposed +// functions of the module. For private functions dealing with the browser's +// graphics library context, see './webGL_curves.ts'. +// ============================================================================= + +// Core functions + +/** + * Makes a Sound with given wave function and duration. + * The wave function is a function: number -> number + * that takes in a non-negative input time and returns an amplitude + * between -1 and 1. + * + * @param wave wave function of the sound + * @param duration duration of the sound + * @return with wave as wave function and duration as duration + * @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); + */ +export function make_sound(wave: Wave, duration: number): Sound { + return pair((t: number) => (t >= duration ? 0 : wave(t)), duration); +} + +/** + * Accesses the wave function of a given Sound. + * + * @param sound given Sound + * @return the wave function of the Sound + * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + */ +export function get_wave(sound: Sound): Wave { + return head(sound); +} + +/** + * Accesses the duration of a given Sound. + * + * @param sound given Sound + * @return the duration of the Sound + * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + */ +export function get_duration(sound: Sound): number { + return tail(sound); +} + +/** + * Checks if the argument is a Sound + * + * @param x input to be checked + * @return true if x is a Sound, false otherwise + * @example is_sound(make_sound(t => 0, 2)); // Returns true + */ +export function is_sound(x: any): boolean { + return ( + is_pair(x) + && typeof get_wave(x) === 'function' + && typeof get_duration(x) === 'number' + ); +} + +/** + * Plays the given Wave using the computer’s sound device, for the duration + * given in seconds. + * The sound is only played if no other sounds are currently being played. + * + * @param wave the wave function to play, starting at 0 + * @return the given sound + * @example play_wave(t => math_sin(t * 3000), 5); + */ +export function play_wave(wave: Wave, duration: number): AudioPlayed { + return play(make_sound(wave, duration)); +} + +/** + * Plays the given Sound using the computer’s sound device. + * The sound is only played if no other sounds are currently being played. + * + * @param sound the sound to play + * @return the given sound + * @example play(sine_sound(440, 5)); + */ +export function play(sound: Sound): AudioPlayed { + // Type-check sound + if (!is_sound(sound)) { + throw new Error(`play is expecting sound, but encountered ${sound}`); + // If a sound is already playing, terminate execution. + } else if (isPlaying) { + throw new Error('play: audio system still playing previous sound'); + } else if (get_duration(sound) < 0) { + throw new Error('play: duration of sound is negative'); + } else { + // Instantiate audio context if it has not been instantiated. + if (!audioplayer) { + init_audioCtx(); + } + + // Create mono buffer + const channel: number[] = []; + const len = Math.ceil(FS * get_duration(sound)); + + let temp: number; + let prev_value = 0; + + const wave = get_wave(sound); + for (let i = 0; i < len; i += 1) { + temp = wave(i / FS); + // clip amplitude + // channel[i] = temp > 1 ? 1 : temp < -1 ? -1 : temp; + if (temp > 1) { + channel[i] = 1; + } else if (temp < -1) { + channel[i] = -1; + } else { + channel[i] = temp; + } + + // smoothen out sudden cut-outs + if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { + channel[i] = prev_value * 0.999; + } + + prev_value = channel[i]; + } + + // quantize + for (let i = 0; i < channel.length; i += 1) { + channel[i] = Math.floor(channel[i] * 32767.999); + } + + const riffwave = new RIFFWAVE([]); + riffwave.header.sampleRate = FS; + riffwave.header.numChannels = 1; + riffwave.header.bitsPerSample = 16; + riffwave.Make(channel); + + /* + const audio = new Audio(riffwave.dataURI); + const source2 = audioplayer.createMediaElementSource(audio); + source2.connect(audioplayer.destination); + + // Connect data to output destination + isPlaying = true; + audio.play(); + audio.onended = () => { + source2.disconnect(audioplayer.destination); + isPlaying = false; + }; */ + + const soundToPlay = { + toReplString: () => '', + dataUri: riffwave.dataURI, + }; + audioPlayed.push(soundToPlay); + return soundToPlay; + } +} + +/** + * Plays the given Sound using the computer’s sound device + * on top of any sounds that are currently playing. + * + * @param sound the sound to play + * @example play_concurrently(sine_sound(440, 5)); + */ +export function play_concurrently(sound: Sound): void { + // Type-check sound + if (!is_sound(sound)) { + throw new Error( + `play_concurrently is expecting sound, but encountered ${sound}`, + ); + } else if (get_duration(sound) <= 0) { + // Do nothing + } else { + // Instantiate audio context if it has not been instantiated. + if (!audioplayer) { + init_audioCtx(); + } + + // Create mono buffer + const theBuffer = audioplayer.createBuffer( + 1, + Math.ceil(FS * get_duration(sound)), + FS, + ); + const channel = theBuffer.getChannelData(0); + + let temp: number; + let prev_value = 0; + + const wave = get_wave(sound); + for (let i = 0; i < channel.length; i += 1) { + temp = wave(i / FS); + // clip amplitude + if (temp > 1) { + channel[i] = 1; + } else if (temp < -1) { + channel[i] = -1; + } else { + channel[i] = temp; + } + + // smoothen out sudden cut-outs + if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { + channel[i] = prev_value * 0.999; + } + + prev_value = channel[i]; + } + + // Connect data to output destination + const source = audioplayer.createBufferSource(); + source.buffer = theBuffer; + source.connect(audioplayer.destination); + isPlaying = true; + source.start(); + source.onended = () => { + source.disconnect(audioplayer.destination); + isPlaying = false; + }; + } +} + +/** + * Stops all currently playing sounds. + */ +export function stop(): void { + audioplayer.close(); + isPlaying = false; +} + +// Primitive sounds + +/** + * Makes a noise sound with given duration + * + * @param duration the duration of the noise sound + * @return resulting noise sound + * @example noise_sound(5); + */ +export function noise_sound(duration: number): Sound { + return make_sound((t) => Math.random() * 2 - 1, duration); +} + +/** + * Makes a silence sound with given duration + * + * @param duration the duration of the silence sound + * @return resulting silence sound + * @example silence_sound(5); + */ +export function silence_sound(duration: number): Sound { + return make_sound((t) => 0, duration); +} + +/** + * Makes a sine wave sound with given frequency and duration + * + * @param freq the frequency of the sine wave sound + * @param duration the duration of the sine wave sound + * @return resulting sine wave sound + * @example sine_sound(440, 5); + */ +export function sine_sound(freq: number, duration: number): Sound { + return make_sound((t) => Math.sin(2 * Math.PI * t * freq), duration); +} + +/** + * Makes a square wave sound with given frequency and duration + * + * @param freq the frequency of the square wave sound + * @param duration the duration of the square wave sound + * @return resulting square wave sound + * @example square_sound(440, 5); + */ +export function square_sound(f: number, duration: number): Sound { + function fourier_expansion_square(t: number) { + let answer = 0; + for (let i = 1; i <= fourier_expansion_level; i += 1) { + answer += Math.sin(2 * Math.PI * (2 * i - 1) * f * t) / (2 * i - 1); + } + return answer; + } + return make_sound( + (t) => (4 / Math.PI) * fourier_expansion_square(t), + duration, + ); +} + +/** + * Makes a triangle wave sound with given frequency and duration + * + * @param freq the frequency of the triangle wave sound + * @param duration the duration of the triangle wave sound + * @return resulting triangle wave sound + * @example triangle_sound(440, 5); + */ +export function triangle_sound(freq: number, duration: number): Sound { + function fourier_expansion_triangle(t: number) { + let answer = 0; + for (let i = 0; i < fourier_expansion_level; i += 1) { + answer + += ((-1) ** i * Math.sin((2 * i + 1) * t * freq * Math.PI * 2)) + / (2 * i + 1) ** 2; + } + return answer; + } + return make_sound( + (t) => (8 / Math.PI / Math.PI) * fourier_expansion_triangle(t), + duration, + ); +} + +/** + * Makes a sawtooth wave sound with given frequency and duration + * + * @param freq the frequency of the sawtooth wave sound + * @param duration the duration of the sawtooth wave sound + * @return resulting sawtooth wave sound + * @example sawtooth_sound(440, 5); + */ +export function sawtooth_sound(freq: number, duration: number): Sound { + function fourier_expansion_sawtooth(t: number) { + let answer = 0; + for (let i = 1; i <= fourier_expansion_level; i += 1) { + answer += Math.sin(2 * Math.PI * i * freq * t) / i; + } + return answer; + } + return make_sound( + (t) => 1 / 2 - (1 / Math.PI) * fourier_expansion_sawtooth(t), + duration, + ); +} + +// Composition Operators + +/** + * Makes a new Sound by combining the sounds in a given list + * where the second sound is appended to the end of the first sound, + * the third sound is appended to the end of the second sound, and + * so on. The effect is that the sounds in the list are joined end-to-end + * + * @param list_of_sounds given list of sounds + * @return the combined Sound + * @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); + */ +export function consecutively(list_of_sounds: List): Sound { + function consec_two(ss1: Sound, ss2: Sound) { + const wave1 = get_wave(ss1); + const wave2 = get_wave(ss2); + const dur1 = get_duration(ss1); + const dur2 = get_duration(ss2); + const new_wave = (t: number) => (t < dur1 ? wave1(t) : wave2(t - dur1)); + return make_sound(new_wave, dur1 + dur2); + } + return accumulate(consec_two, silence_sound(0), list_of_sounds); +} + +/** + * Makes a new Sound by combining the sounds in a given list + * where all the sounds are overlapped on top of each other. + * + * @param list_of_sounds given list of sounds + * @return the combined Sound + * @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))) + */ +export function simultaneously(list_of_sounds: List): Sound { + function simul_two(ss1: Sound, ss2: Sound) { + const wave1 = get_wave(ss1); + const wave2 = get_wave(ss2); + const dur1 = get_duration(ss1); + const dur2 = get_duration(ss2); + // new_wave assumes sound discipline (ie, wave(t) = 0 after t > dur) + const new_wave = (t: number) => wave1(t) + wave2(t); + // new_dur is higher of the two dur + const new_dur = dur1 < dur2 ? dur2 : dur1; + return make_sound(new_wave, new_dur); + } + + const mushed_sounds = accumulate(simul_two, silence_sound(0), list_of_sounds); + const normalised_wave = (t: number) => head(mushed_sounds)(t) / length(list_of_sounds); + const highest_duration = tail(mushed_sounds); + return make_sound(normalised_wave, highest_duration); +} + +/** + * Returns an envelope: a function from Sound to Sound. + * When the adsr envelope is applied to a Sound, it returns + * a new Sound with its amplitude modified according to parameters + * The relative amplitude increases from 0 to 1 linearly over the + * attack proportion, then decreases from 1 to sustain level over the + * decay proportion, and remains at that level until the release + * proportion when it decays back to 0. + * @param attack_ratio proportion of Sound in attack phase + * @param decay_ratio proportion of Sound decay phase + * @param sustain_level sustain level between 0 and 1 + * @param release_ratio proportion of Sound in release phase + * @return Envelope a function from Sound to Sound + * @example adsr(0.2, 0.3, 0.3, 0.1)(sound); + */ +export function adsr( + attack_ratio: number, + decay_ratio: number, + sustain_level: number, + release_ratio: number, +): SoundTransformer { + return (sound) => { + const wave = get_wave(sound); + const duration = get_duration(sound); + const attack_time = duration * attack_ratio; + const decay_time = duration * decay_ratio; + const release_time = duration * release_ratio; + return make_sound((x) => { + if (x < attack_time) { + return wave(x) * (x / attack_time); + } + if (x < attack_time + decay_time) { + return ( + ((1 - sustain_level) * linear_decay(decay_time)(x - attack_time) + + sustain_level) + * wave(x) + ); + } + if (x < duration - release_time) { + return wave(x) * sustain_level; + } + return ( + wave(x) + * sustain_level + * linear_decay(release_time)(x - (duration - release_time)) + ); + }, duration); + }; +} + +/** + * Returns a Sound that results from applying a list of envelopes + * to a given wave form. The wave form is a Sound generator that + * takes a frequency and a duration as arguments and produces a + * Sound with the given frequency and duration. Each envelope is + * applied to a harmonic: the first harmonic has the given frequency, + * the second has twice the frequency, the third three times the + * frequency etc. The harmonics are then layered simultaneously to + * produce the resulting Sound. + * @param waveform function from pair(frequency, duration) to Sound + * @param base_frequency frequency of the first harmonic + * @param duration duration of the produced Sound, in seconds + * @param envelopes – list of envelopes, which are functions from Sound to Sound + * @return Sound resulting Sound + * @example stacking_adsr(sine_sound, 300, 5, list(adsr(0.1, 0.3, 0.2, 0.5), adsr(0.2, 0.5, 0.6, 0.1), adsr(0.3, 0.1, 0.7, 0.3))); + */ +export function stacking_adsr( + waveform: SoundProducer, + base_frequency: number, + duration: number, + envelopes: List, +): Sound { + function zip(lst: List, n: number) { + if (is_null(lst)) { + return lst; + } + return pair(pair(n, head(lst)), zip(tail(lst), n + 1)); + } + + return simultaneously( + accumulate( + (x: any, y: any) => pair(tail(x)(waveform(base_frequency * head(x), duration)), y), + null, + zip(envelopes, 1), + ), + ); +} + +/** + * Returns a SoundTransformer which uses its argument + * to modulate the phase of a (carrier) sine wave + * of given frequency and duration with a given Sound. + * Modulating with a low frequency Sound results in a vibrato effect. + * Modulating with a Sound with frequencies comparable to + * the sine wave frequency results in more complex wave forms. + * + * @param freq the frequency of the sine wave to be modulated + * @param duration the duration of the output soud + * @param amount the amount of modulation to apply to the carrier sine wave + * @return function which takes in a Sound and returns a Sound + * @example phase_mod(440, 5, 1)(sine_sound(220, 5)); + */ +export function phase_mod( + freq: number, + duration: number, + amount: number, +): SoundTransformer { + return (modulator: Sound) => make_sound( + (t) => Math.sin(2 * Math.PI * t * freq + amount * get_wave(modulator)(t)), + duration, + ); +} + +// MIDI conversion functions + +/** + * Converts a letter name to its corresponding MIDI note. + * The letter name is represented in standard pitch notation. + * Examples are "A5", "Db3", "C#7". + * Refer to
    this mapping from + * letter name to midi notes. + * + * @param letter_name given letter name + * @return the corresponding midi note + * @example letter_name_to_midi_note("C4"); // Returns 60 + */ +export function letter_name_to_midi_note(note: string): number { + let res = 12; // C0 is midi note 12 + const n = note[0].toUpperCase(); + switch (n) { + case 'D': + res += 2; + break; + + case 'E': + res += 4; + break; + + case 'F': + res += 5; + break; + + case 'G': + res += 7; + break; + + case 'A': + res += 9; + break; + + case 'B': + res += 11; + break; + + default: + break; + } + + if (note.length === 2) { + res += parseInt(note[1]) * 12; + } else if (note.length === 3) { + switch (note[1]) { + case '#': + res += 1; + break; + + case 'b': + res -= 1; + break; + + default: + break; + } + res += parseInt(note[2]) * 12; + } + return res; +} + +/** + * Converts a MIDI note to its corresponding frequency. + * + * @param note given MIDI note + * @return the frequency of the MIDI note + * @example midi_note_to_frequency(69); // Returns 440 + */ +export function midi_note_to_frequency(note: number): number { + // A4 = 440Hz = midi note 69 + return 440 * 2 ** ((note - 69) / 12); +} + +/** + * Converts a letter name to its corresponding frequency. + * + * @param letter_name given letter name + * @return the corresponding frequency + * @example letter_name_to_frequency("A4"); // Returns 440 + */ +export function letter_name_to_frequency(note: string): number { + return midi_note_to_frequency(letter_name_to_midi_note(note)); +} + +// Instruments + +/** + * returns a Sound reminiscent of a bell, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting bell Sound with given pitch and duration + * @example bell(40, 1); + */ +export function bell(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list( + adsr(0, 0.6, 0, 0.05), + adsr(0, 0.6618, 0, 0.05), + adsr(0, 0.7618, 0, 0.05), + adsr(0, 0.9071, 0, 0.05), + ), + ); +} + +/** + * returns a Sound reminiscent of a cello, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting cello Sound with given pitch and duration + * @example cello(36, 5); + */ +export function cello(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0.05, 0, 1, 0.1), adsr(0.05, 0, 1, 0.15), adsr(0, 0, 0.2, 0.15)), + ); +} + +/** + * returns a Sound reminiscent of a piano, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting piano Sound with given pitch and duration + * @example piano(48, 5); + */ +export function piano(note: number, duration: number): Sound { + return stacking_adsr( + triangle_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0, 0.515, 0, 0.05), adsr(0, 0.32, 0, 0.05), adsr(0, 0.2, 0, 0.05)), + ); +} + +/** + * returns a Sound reminiscent of a trombone, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting trombone Sound with given pitch and duration + * @example trombone(60, 2); + */ +export function trombone(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0.2, 0, 1, 0.1), adsr(0.3236, 0.6, 0, 0.1)), + ); +} + +/** + * returns a Sound reminiscent of a violin, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting violin Sound with given pitch and duration + * @example violin(53, 4); + */ +export function violin(note: number, duration: number): Sound { + return stacking_adsr( + sawtooth_sound, + midi_note_to_frequency(note), + duration, + list( + adsr(0.35, 0, 1, 0.15), + adsr(0.35, 0, 1, 0.15), + adsr(0.45, 0, 1, 0.15), + adsr(0.45, 0, 1, 0.15), + ), + ); +} diff --git a/src/bundles/sound/index.ts b/src/bundles/sound/index.ts index 1d8027477..1a57a7d0b 100644 --- a/src/bundles/sound/index.ts +++ b/src/bundles/sound/index.ts @@ -1,107 +1,107 @@ -import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; -import { - adsr, - audioPlayed, - // Instruments - bell, - cello, - // Composition and Envelopes - consecutively, - get_duration, - get_wave, - // Recording - init_record, - is_sound, - letter_name_to_frequency, - // MIDI - letter_name_to_midi_note, - // Constructor/Accessors/Typecheck - make_sound, - midi_note_to_frequency, - // Basic waveforms - noise_sound, - phase_mod, - piano, - // Play-related - play, - play_concurrently, - play_wave, - record, - record_for, - sawtooth_sound, - silence_sound, - simultaneously, - sine_sound, - square_sound, - stacking_adsr, - stop, - triangle_sound, - trombone, - violin, -} from './functions'; -import { SoundsModuleState } from './types'; - -export default function sounds( - moduleParams: ModuleParams, - moduleContexts: ModuleContexts -) { - // Update the module's global context - let moduleContext = moduleContexts.get('sound'); - - if (!moduleContext) { - moduleContext = { - tabs: [], - state: { - audioPlayed, - }, - }; - - moduleContexts.set('sound', moduleContext); - } else if (!moduleContext.state) { - moduleContext.state = { - audioPlayed, - }; - } else { - (moduleContext.state as SoundsModuleState).audioPlayed = audioPlayed; - } - - return { - // Constructor/Accessors/Typecheck - make_sound, - get_wave, - get_duration, - is_sound, - // Play-related - play, - play_wave, - play_concurrently, - stop, - // Recording - init_record, - record, - record_for, - // Composition and Envelopes - consecutively, - simultaneously, - phase_mod, - adsr, - stacking_adsr, - // Basic waveforms - noise_sound, - silence_sound, - sine_sound, - sawtooth_sound, - triangle_sound, - square_sound, - // MIDI - letter_name_to_midi_note, - midi_note_to_frequency, - letter_name_to_frequency, - // Instruments - bell, - cello, - piano, - trombone, - violin, - }; -} +import { ModuleContexts, ModuleParams } from '../../typings/type_helpers.js'; +import { + adsr, + audioPlayed, + // Instruments + bell, + cello, + // Composition and Envelopes + consecutively, + get_duration, + get_wave, + // Recording + init_record, + is_sound, + letter_name_to_frequency, + // MIDI + letter_name_to_midi_note, + // Constructor/Accessors/Typecheck + make_sound, + midi_note_to_frequency, + // Basic waveforms + noise_sound, + phase_mod, + piano, + // Play-related + play, + play_concurrently, + play_wave, + record, + record_for, + sawtooth_sound, + silence_sound, + simultaneously, + sine_sound, + square_sound, + stacking_adsr, + stop, + triangle_sound, + trombone, + violin, +} from './functions'; +import { SoundsModuleState } from './types'; + +export default function sounds( + moduleParams: ModuleParams, + moduleContexts: ModuleContexts, +) { + // Update the module's global context + let moduleContext = moduleContexts.get('sound'); + + if (!moduleContext) { + moduleContext = { + tabs: [], + state: { + audioPlayed, + }, + }; + + moduleContexts.set('sound', moduleContext); + } else if (!moduleContext.state) { + moduleContext.state = { + audioPlayed, + }; + } else { + (moduleContext.state as SoundsModuleState).audioPlayed = audioPlayed; + } + + return { + // Constructor/Accessors/Typecheck + make_sound, + get_wave, + get_duration, + is_sound, + // Play-related + play, + play_wave, + play_concurrently, + stop, + // Recording + init_record, + record, + record_for, + // Composition and Envelopes + consecutively, + simultaneously, + phase_mod, + adsr, + stacking_adsr, + // Basic waveforms + noise_sound, + silence_sound, + sine_sound, + sawtooth_sound, + triangle_sound, + square_sound, + // MIDI + letter_name_to_midi_note, + midi_note_to_frequency, + letter_name_to_frequency, + // Instruments + bell, + cello, + piano, + trombone, + violin, + }; +} diff --git a/src/bundles/sound/list.ts b/src/bundles/sound/list.ts index e5205937c..d2ae0927d 100644 --- a/src/bundles/sound/list.ts +++ b/src/bundles/sound/list.ts @@ -1,372 +1,372 @@ -/* eslint-disable @typescript-eslint/naming-convention, no-else-return, prefer-template, no-param-reassign, no-plusplus, operator-assignment, no-lonely-if */ -/* prettier-ignore */ -// list.js: Supporting lists in the Scheme style, using pairs made -// up of two-element JavaScript array (vector) - -// Author: Martin Henz - -// Note: this library is used in the externalLibs of cadet-frontend. -// It is distinct from the LISTS library of Source §2, which contains -// primitive and predeclared functions from Chapter 2 of SICP JS. - -// array test works differently for Rhino and -// the Firefox environment (especially Web Console) -export function array_test(x) : boolean { - if (Array.isArray === undefined) { - return x instanceof Array - } else { - return Array.isArray(x) - } -} - -// pair constructs a pair using a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function pair(x, xs): [any, any] { - return [x, xs]; -} - -// is_pair returns true iff arg is a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_pair(x): boolean { - return array_test(x) && x.length === 2; -} - -// head returns the first component of the given pair, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function head(xs): any { - if (is_pair(xs)) { - return xs[0]; - } else { - throw new Error( - 'head(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// tail returns the second component of the given pair -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function tail(xs) { - if (is_pair(xs)) { - return xs[1]; - } else { - throw new Error( - 'tail(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// is_null returns true if arg is exactly null -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_null(xs) { - return xs === null; -} - -// is_list recurses down the list and checks that it ends with the empty list [] -// does not throw Value exceptions -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_list(xs) { - for (; ; xs = tail(xs)) { - if (is_null(xs)) { - return true; - } else if (!is_pair(xs)) { - return false; - } - } -} - -// list makes a list out of its arguments -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list(...args) { - let the_list: any = null; - for (let i = args.length - 1; i >= 0; i--) { - the_list = pair(args[i], the_list); - } - return the_list; -} - -// list_to_vector returns vector that contains the elements of the argument list -// in the given order. -// list_to_vector throws an exception if the argument is not a list -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list_to_vector(lst) { - const vector: any[] = []; - while (!is_null(lst)) { - vector.push(head(lst)); - lst = tail(lst); - } - return vector; -} - -// vector_to_list returns a list that contains the elements of the argument vector -// in the given order. -// vector_to_list throws an exception if the argument is not a vector -// LOW-LEVEL FUNCTION, NOT SOURCE -export function vector_to_list(vector) { - let result: any = null; - for (let i = vector.length - 1; i >= 0; i = i - 1) { - result = pair(vector[i], result); - } - return result; -} - -// returns the length of a given argument list -// throws an exception if the argument is not a list -export function length(xs) { - let i = 0; - while (!is_null(xs)) { - i += 1; - xs = tail(xs); - } - return i; -} - -// map applies first arg f to the elements of the second argument, -// assumed to be a list. -// f is applied element-by-element: -// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] -// map throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the first -// argument is not a function. -// tslint:disable-next-line:ban-types -export function map(f, xs) { - return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); -} - -// build_list takes a non-negative integer n as first argument, -// and a function fun as second argument. -// build_list returns a list of n elements, that results from -// applying fun to the numbers from 0 to n-1. -// tslint:disable-next-line:ban-types -export function build_list(n, fun) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'build_list(n, fun) expects a positive integer as ' + - 'argument n, but encountered ' + - n - ); - } - - // tslint:disable-next-line:ban-types - function build(i, alreadyBuilt) { - if (i < 0) { - return alreadyBuilt; - } else { - return build(i - 1, pair(fun(i), alreadyBuilt)); - } - } - - return build(n - 1, null); -} - -// for_each applies first arg fun to the elements of the list passed as -// second argument. fun is applied element-by-element: -// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). -// for_each returns true. -// for_each throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the -// first argument is not a function. -// tslint:disable-next-line:ban-types -export function for_each(fun, xs) { - if (!is_list(xs)) { - throw new Error( - 'for_each expects a list as argument xs, but encountered ' + xs - ); - } - for (; !is_null(xs); xs = tail(xs)) { - fun(head(xs)); - } - return true; -} - -// reverse reverses the argument list -// reverse throws an exception if the argument is not a list. -export function reverse(xs) { - if (!is_list(xs)) { - throw new Error( - 'reverse(xs) expects a list as argument xs, but encountered ' + xs - ); - } - let result: any = null; - for (; !is_null(xs); xs = tail(xs)) { - result = pair(head(xs), result); - } - return result; -} - -// append first argument list and second argument list. -// In the result, the [] at the end of the first argument list -// is replaced by the second argument list -// append throws an exception if the first argument is not a list -export function append(xs, ys) { - if (is_null(xs)) { - return ys; - } else { - return pair(head(xs), append(tail(xs), ys)); - } -} - -// member looks for a given first-argument element in a given -// second argument list. It returns the first postfix sublist -// that starts with the given element. It returns [] if the -// element does not occur in the list -export function member(v, xs) { - for (; !is_null(xs); xs = tail(xs)) { - if (head(xs) === v) { - return xs; - } - } - return null; -} - -// removes the first occurrence of a given first-argument element -// in a given second-argument list. Returns the original list -// if there is no occurrence. -export function remove(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return tail(xs); - } else { - return pair(head(xs), remove(v, tail(xs))); - } - } -} - -// Similar to remove. But removes all instances of v instead of just the first -export function remove_all(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return remove_all(v, tail(xs)); - } else { - return pair(head(xs), remove_all(v, tail(xs))); - } - } -} - -// for backwards-compatibility -// equal computes the structural equality -// over its arguments -export function equal(item1, item2) { - if (is_pair(item1) && is_pair(item2)) { - return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); - } else { - return item1 === item2; - } -} - -// assoc treats the second argument as an association, -// a list of (index,value) pairs. -// assoc returns the first (index,value) pair whose -// index equal (using structural equality) to the given -// first argument v. Returns false if there is no such -// pair -export function assoc(v, xs) { - if (is_null(xs)) { - return false; - } else if (equal(v, head(head(xs)))) { - return head(xs); - } else { - return assoc(v, tail(xs)); - } -} - -// filter returns the sublist of elements of given list xs -// for which the given predicate function returns true. -// tslint:disable-next-line:ban-types -export function filter(pred, xs) { - if (is_null(xs)) { - return xs; - } else { - if (pred(head(xs))) { - return pair(head(xs), filter(pred, tail(xs))); - } else { - return filter(pred, tail(xs)); - } - } -} - -// enumerates numbers starting from start, -// using a step size of 1, until the number -// exceeds end. -export function enum_list(start, end) { - if (typeof start !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - start - ); - } - if (typeof end !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - end - ); - } - if (start > end) { - return null; - } else { - return pair(start, enum_list(start + 1, end)); - } -} - -// Returns the item in list lst at index n (the first item is at position 0) -export function list_ref(xs, n) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'list_ref(xs, n) expects a positive integer as argument n, but encountered ' + - n - ); - } - for (; n > 0; --n) { - xs = tail(xs); - } - return head(xs); -} - -// accumulate applies given operation op to elements of a list -// in a right-to-left order, first apply op to the last element -// and an initial element, resulting in r1, then to the -// second-last element and r1, resulting in r2, etc, and finally -// to the first element and r_n-1, where n is the length of the -// list. -// accumulate(op,zero,list(1,2,3)) results in -// op(1, op(2, op(3, zero))) -export function accumulate(op, initial, sequence) { - if (is_null(sequence)) { - return initial; - } else { - return op(head(sequence), accumulate(op, initial, tail(sequence))); - } -} - -// set_head(xs,x) changes the head of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_head(xs, x) { - if (is_pair(xs)) { - xs[0] = x; - return undefined; - } else { - throw new Error( - 'set_head(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// set_tail(xs,x) changes the tail of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_tail(xs, x) { - if (is_pair(xs)) { - xs[1] = x; - return undefined; - } else { - throw new Error( - 'set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} +/* eslint-disable @typescript-eslint/naming-convention, no-else-return, prefer-template, no-param-reassign, no-plusplus, operator-assignment, no-lonely-if */ +/* prettier-ignore */ +// list.js: Supporting lists in the Scheme style, using pairs made +// up of two-element JavaScript array (vector) + +// Author: Martin Henz + +// Note: this library is used in the externalLibs of cadet-frontend. +// It is distinct from the LISTS library of Source §2, which contains +// primitive and predeclared functions from Chapter 2 of SICP JS. + +// array test works differently for Rhino and +// the Firefox environment (especially Web Console) +export function array_test(x) : boolean { + if (Array.isArray === undefined) { + return x instanceof Array; + } else { + return Array.isArray(x); + } +} + +// pair constructs a pair using a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function pair(x, xs): [any, any] { + return [x, xs]; +} + +// is_pair returns true iff arg is a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_pair(x): boolean { + return array_test(x) && x.length === 2; +} + +// head returns the first component of the given pair, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function head(xs): any { + if (is_pair(xs)) { + return xs[0]; + } else { + throw new Error( + 'head(xs) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// tail returns the second component of the given pair +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function tail(xs) { + if (is_pair(xs)) { + return xs[1]; + } else { + throw new Error( + 'tail(xs) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// is_null returns true if arg is exactly null +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_null(xs) { + return xs === null; +} + +// is_list recurses down the list and checks that it ends with the empty list [] +// does not throw Value exceptions +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_list(xs) { + for (; ; xs = tail(xs)) { + if (is_null(xs)) { + return true; + } else if (!is_pair(xs)) { + return false; + } + } +} + +// list makes a list out of its arguments +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list(...args) { + let the_list: any = null; + for (let i = args.length - 1; i >= 0; i--) { + the_list = pair(args[i], the_list); + } + return the_list; +} + +// list_to_vector returns vector that contains the elements of the argument list +// in the given order. +// list_to_vector throws an exception if the argument is not a list +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list_to_vector(lst) { + const vector: any[] = []; + while (!is_null(lst)) { + vector.push(head(lst)); + lst = tail(lst); + } + return vector; +} + +// vector_to_list returns a list that contains the elements of the argument vector +// in the given order. +// vector_to_list throws an exception if the argument is not a vector +// LOW-LEVEL FUNCTION, NOT SOURCE +export function vector_to_list(vector) { + let result: any = null; + for (let i = vector.length - 1; i >= 0; i = i - 1) { + result = pair(vector[i], result); + } + return result; +} + +// returns the length of a given argument list +// throws an exception if the argument is not a list +export function length(xs) { + let i = 0; + while (!is_null(xs)) { + i += 1; + xs = tail(xs); + } + return i; +} + +// map applies first arg f to the elements of the second argument, +// assumed to be a list. +// f is applied element-by-element: +// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] +// map throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the first +// argument is not a function. +// tslint:disable-next-line:ban-types +export function map(f, xs) { + return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); +} + +// build_list takes a non-negative integer n as first argument, +// and a function fun as second argument. +// build_list returns a list of n elements, that results from +// applying fun to the numbers from 0 to n-1. +// tslint:disable-next-line:ban-types +export function build_list(n, fun) { + if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { + throw new Error( + 'build_list(n, fun) expects a positive integer as ' + + 'argument n, but encountered ' + + n, + ); + } + + // tslint:disable-next-line:ban-types + function build(i, alreadyBuilt) { + if (i < 0) { + return alreadyBuilt; + } else { + return build(i - 1, pair(fun(i), alreadyBuilt)); + } + } + + return build(n - 1, null); +} + +// for_each applies first arg fun to the elements of the list passed as +// second argument. fun is applied element-by-element: +// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). +// for_each returns true. +// for_each throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the +// first argument is not a function. +// tslint:disable-next-line:ban-types +export function for_each(fun, xs) { + if (!is_list(xs)) { + throw new Error( + 'for_each expects a list as argument xs, but encountered ' + xs, + ); + } + for (; !is_null(xs); xs = tail(xs)) { + fun(head(xs)); + } + return true; +} + +// reverse reverses the argument list +// reverse throws an exception if the argument is not a list. +export function reverse(xs) { + if (!is_list(xs)) { + throw new Error( + 'reverse(xs) expects a list as argument xs, but encountered ' + xs, + ); + } + let result: any = null; + for (; !is_null(xs); xs = tail(xs)) { + result = pair(head(xs), result); + } + return result; +} + +// append first argument list and second argument list. +// In the result, the [] at the end of the first argument list +// is replaced by the second argument list +// append throws an exception if the first argument is not a list +export function append(xs, ys) { + if (is_null(xs)) { + return ys; + } else { + return pair(head(xs), append(tail(xs), ys)); + } +} + +// member looks for a given first-argument element in a given +// second argument list. It returns the first postfix sublist +// that starts with the given element. It returns [] if the +// element does not occur in the list +export function member(v, xs) { + for (; !is_null(xs); xs = tail(xs)) { + if (head(xs) === v) { + return xs; + } + } + return null; +} + +// removes the first occurrence of a given first-argument element +// in a given second-argument list. Returns the original list +// if there is no occurrence. +export function remove(v, xs) { + if (is_null(xs)) { + return null; + } else { + if (v === head(xs)) { + return tail(xs); + } else { + return pair(head(xs), remove(v, tail(xs))); + } + } +} + +// Similar to remove. But removes all instances of v instead of just the first +export function remove_all(v, xs) { + if (is_null(xs)) { + return null; + } else { + if (v === head(xs)) { + return remove_all(v, tail(xs)); + } else { + return pair(head(xs), remove_all(v, tail(xs))); + } + } +} + +// for backwards-compatibility +// equal computes the structural equality +// over its arguments +export function equal(item1, item2) { + if (is_pair(item1) && is_pair(item2)) { + return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); + } else { + return item1 === item2; + } +} + +// assoc treats the second argument as an association, +// a list of (index,value) pairs. +// assoc returns the first (index,value) pair whose +// index equal (using structural equality) to the given +// first argument v. Returns false if there is no such +// pair +export function assoc(v, xs) { + if (is_null(xs)) { + return false; + } else if (equal(v, head(head(xs)))) { + return head(xs); + } else { + return assoc(v, tail(xs)); + } +} + +// filter returns the sublist of elements of given list xs +// for which the given predicate function returns true. +// tslint:disable-next-line:ban-types +export function filter(pred, xs) { + if (is_null(xs)) { + return xs; + } else { + if (pred(head(xs))) { + return pair(head(xs), filter(pred, tail(xs))); + } else { + return filter(pred, tail(xs)); + } + } +} + +// enumerates numbers starting from start, +// using a step size of 1, until the number +// exceeds end. +export function enum_list(start, end) { + if (typeof start !== 'number') { + throw new Error( + 'enum_list(start, end) expects a number as argument start, but encountered ' + + start, + ); + } + if (typeof end !== 'number') { + throw new Error( + 'enum_list(start, end) expects a number as argument start, but encountered ' + + end, + ); + } + if (start > end) { + return null; + } else { + return pair(start, enum_list(start + 1, end)); + } +} + +// Returns the item in list lst at index n (the first item is at position 0) +export function list_ref(xs, n) { + if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { + throw new Error( + 'list_ref(xs, n) expects a positive integer as argument n, but encountered ' + + n, + ); + } + for (; n > 0; --n) { + xs = tail(xs); + } + return head(xs); +} + +// accumulate applies given operation op to elements of a list +// in a right-to-left order, first apply op to the last element +// and an initial element, resulting in r1, then to the +// second-last element and r1, resulting in r2, etc, and finally +// to the first element and r_n-1, where n is the length of the +// list. +// accumulate(op,zero,list(1,2,3)) results in +// op(1, op(2, op(3, zero))) +export function accumulate(op, initial, sequence) { + if (is_null(sequence)) { + return initial; + } else { + return op(head(sequence), accumulate(op, initial, tail(sequence))); + } +} + +// set_head(xs,x) changes the head of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function set_head(xs, x) { + if (is_pair(xs)) { + xs[0] = x; + return undefined; + } else { + throw new Error( + 'set_head(xs,x) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// set_tail(xs,x) changes the tail of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function set_tail(xs, x) { + if (is_pair(xs)) { + xs[1] = x; + return undefined; + } else { + throw new Error( + 'set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs, + ); + } +} diff --git a/src/bundles/sound/riffwave.ts b/src/bundles/sound/riffwave.ts index 70a1af751..478588146 100644 --- a/src/bundles/sound/riffwave.ts +++ b/src/bundles/sound/riffwave.ts @@ -1,22 +1,22 @@ -/* - * RIFFWAVE.js v0.03 - Audio encoder for HTML5 this mapping from - * letter name to midi notes. - * - * @param letter_name given letter name - * @return the corresponding midi note - * @example letter_name_to_midi_note("C4"); // Returns 60 - */ -export function letter_name_to_midi_note(note: string): number { - let res = 12; // C0 is midi note 12 - const n = note[0].toUpperCase(); - switch (n) { - case 'D': - res += 2; - break; - - case 'E': - res += 4; - break; - - case 'F': - res += 5; - break; - - case 'G': - res += 7; - break; - - case 'A': - res += 9; - break; - - case 'B': - res += 11; - break; - - default: - break; - } - - if (note.length === 2) { - res += parseInt(note[1]) * 12; - } else if (note.length === 3) { - switch (note[1]) { - case '#': - res += 1; - break; - - case 'b': - res -= 1; - break; - - default: - break; - } - res += parseInt(note[2]) * 12; - } - return res; -} - -/** - * Converts a MIDI note to its corresponding frequency. - * - * @param note given MIDI note - * @return the frequency of the MIDI note - * @example midi_note_to_frequency(69); // Returns 440 - */ -export function midi_note_to_frequency(note: number): number { - // A4 = 440Hz = midi note 69 - return 440 * 2 ** ((note - 69) / 12); -} - -/** - * Converts a letter name to its corresponding frequency. - * - * @param letter_name given letter name - * @return the corresponding frequency - * @example letter_name_to_frequency("A4"); // Returns 440 - */ -export function letter_name_to_frequency(note: string): number { - return midi_note_to_frequency(letter_name_to_midi_note(note)); -} - -// Instruments - -/** - * returns a Sound reminiscent of a bell, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting bell Sound with given pitch and duration - * @example bell(40, 1); - */ -export function bell(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list( - adsr(0, 0.6, 0, 0.05), - adsr(0, 0.6618, 0, 0.05), - adsr(0, 0.7618, 0, 0.05), - adsr(0, 0.9071, 0, 0.05) - ) - ); -} - -/** - * returns a Sound reminiscent of a cello, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting cello Sound with given pitch and duration - * @example cello(36, 5); - */ -export function cello(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0.05, 0, 1, 0.1), adsr(0.05, 0, 1, 0.15), adsr(0, 0, 0.2, 0.15)) - ); -} - -/** - * returns a Sound reminiscent of a piano, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting piano Sound with given pitch and duration - * @example piano(48, 5); - */ -export function piano(note: number, duration: number): Sound { - return stacking_adsr( - triangle_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0, 0.515, 0, 0.05), adsr(0, 0.32, 0, 0.05), adsr(0, 0.2, 0, 0.05)) - ); -} - -/** - * returns a Sound reminiscent of a trombone, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting trombone Sound with given pitch and duration - * @example trombone(60, 2); - */ -export function trombone(note: number, duration: number): Sound { - return stacking_adsr( - square_sound, - midi_note_to_frequency(note), - duration, - list(adsr(0.2, 0, 1, 0.1), adsr(0.3236, 0.6, 0, 0.1)) - ); -} - -/** - * returns a Sound reminiscent of a violin, playing - * a given note for a given duration - * @param note MIDI note - * @param duration duration in seconds - * @return Sound resulting violin Sound with given pitch and duration - * @example violin(53, 4); - */ -export function violin(note: number, duration: number): Sound { - return stacking_adsr( - sawtooth_sound, - midi_note_to_frequency(note), - duration, - list( - adsr(0.35, 0, 1, 0.15), - adsr(0.35, 0, 1, 0.15), - adsr(0.45, 0, 1, 0.15), - adsr(0.45, 0, 1, 0.15) - ) - ); -} +/** + * + * The stereo sounds library build on the sounds library by accommodating stereo sounds. + * Within this library, all sounds are represented in stereo, with two waves, left and right. + * + * A Stereo Sound is a `pair(pair(left_wave, right_wave), duration)` where duration is the length of the sound in seconds. + * The constructor `make_stereo_sound` and accessors `get_left_wave`, `get_right_wave`, and `get_duration` are provided. + * The `make_sound` constructor from sounds is syntatic sugar for `make_stereo_sounds` with equal waves. + * + * @module stereo_sound + * @author Koh Shang Hui + * @author Samyukta Sounderraman + */ + +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-use-before-define, @typescript-eslint/no-unused-vars, new-cap */ +import { + accumulate, + head, + is_null, + is_pair, + length, + list, + pair, + tail, +} from './list'; +import { RIFFWAVE } from './riffwave'; +import { + AudioPlayed, + List, + Sound, + SoundProducer, + SoundTransformer, + Wave, +} from './types'; + +// Global Constants and Variables + +const FS: number = 44100; // Output sample rate +const fourier_expansion_level: number = 5; // fourier expansion level + +/** @hidden */ +export const audioPlayed: AudioPlayed[] = []; + +let audioElement: HTMLAudioElement; +// Singular audio context for all playback functions +let audioplayer: AudioContext; + +// Track if a sound is currently playing +let isPlaying: boolean; + +// Instantiates new audio context +function init_audioCtx(): void { + audioplayer = new window.AudioContext(); + // audioplayer = new (window.AudioContext || window.webkitAudioContext)(); +} + +// linear decay from 1 to 0 over decay_period +function linear_decay(decay_period: number): (t: number) => number { + return (t) => { + if (t > decay_period || t < 0) { + return 0; + } + return 1 - t / decay_period; + }; +} + +// // --------------------------------------------- +// // Microphone Functionality +// // --------------------------------------------- + +// permission initially undefined +// set to true by granting microphone permission +// set to false by denying microphone permission +let permission: boolean | undefined; + +let recorded_sound: Sound | undefined; + +// check_permission is called whenever we try +// to record a sound +function check_permission() { + if (permission === undefined) { + throw new Error( + 'Call init_record(); to obtain permission to use microphone', + ); + } else if (permission === false) { + throw new Error(`Permission has been denied.\n + Re-start browser and call init_record();\n + to obtain permission to use microphone.`); + } // (permission === true): do nothing +} + +let globalStream: any; + +function rememberStream(stream: any) { + permission = true; + globalStream = stream; +} + +function setPermissionToFalse() { + permission = false; +} + +function start_recording(mediaRecorder: MediaRecorder) { + const data: any[] = []; + // eslint-disable-next-line no-param-reassign + mediaRecorder.ondataavailable = (e) => e.data.size && data.push(e.data); + mediaRecorder.start(); + // eslint-disable-next-line no-param-reassign + mediaRecorder.onstop = () => process(data); +} + +// there is a beep signal at the beginning and end +// of each recording +const recording_signal_duration_ms = 100; + +function play_recording_signal() { + play(sine_sound(1200, recording_signal_duration_ms / 1000)); +} + +// eslint-disable-next-line @typescript-eslint/no-shadow +function process(data: any[] | undefined) { + const audioContext = new AudioContext(); + const blob = new Blob(data); + + convertToArrayBuffer(blob) + .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer)) + .then(save); +} + +// Converts input microphone sound (blob) into array format. +function convertToArrayBuffer(blob: Blob): Promise { + const url = URL.createObjectURL(blob); + return fetch(url) + .then((response) => response.arrayBuffer()); +} + +function save(audioBuffer: AudioBuffer) { + const array = audioBuffer.getChannelData(0); + const duration = array.length / FS; + recorded_sound = make_sound((t) => { + const index = t * FS; + const lowerIndex = Math.floor(index); + const upperIndex = lowerIndex + 1; + const ratio = index - lowerIndex; + const upper = array[upperIndex] ? array[upperIndex] : 0; + const lower = array[lowerIndex] ? array[lowerIndex] : 0; + return lower * (1 - ratio) + upper * ratio; + }, duration); +} + +/** + * Initialize recording by obtaining permission + * to use the default device microphone + * + * @returns string "obtaining recording permission" + */ +export function init_record(): string { + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(rememberStream, setPermissionToFalse); + return 'obtaining recording permission'; +} + +/** + * takes a buffer duration (in seconds) as argument, and + * returns a nullary stop function stop. A call + * stop() returns a sound promise: a nullary function + * that returns a sound. Example:
    init_record();
    + * const stop = record(0.5);
    + * // record after 0.5 seconds. Then in next query:
    + * const promise = stop();
    + * // In next query, you can play the promised sound, by
    + * // applying the promise:
    + * play(promise());
    + * @param buffer - pause before recording, in seconds + * @returns nullary stop function; + * stop() stops the recording and + * returns a sound promise: a nullary function that returns the recorded sound + */ +export function record(buffer: number): () => () => Sound { + check_permission(); + const mediaRecorder = new MediaRecorder(globalStream); + setTimeout(() => { + play_recording_signal(); + start_recording(mediaRecorder); + }, recording_signal_duration_ms + buffer * 1000); + return () => { + mediaRecorder.stop(); + play_recording_signal(); + return () => { + if (recorded_sound === undefined) { + throw new Error('recording still being processed'); + } else { + return recorded_sound; + } + }; + }; +} + +/** + * Records a sound of given duration in seconds, after + * a buffer also in seconds, and + * returns a sound promise: a nullary function + * that returns a sound. Example:
    init_record();
    + * const promise = record_for(2, 0.5);
    + * // In next query, you can play the promised sound, by
    + * // applying the promise:
    + * play(promise());
    + * @param duration duration in seconds + * @param buffer pause before recording, in seconds + * @return promise: nullary function which returns the recorded sound + */ +export function record_for(duration: number, buffer: number): () => Sound { + recorded_sound = undefined; + const duration_ms = duration * 1000; + check_permission(); + const mediaRecorder = new MediaRecorder(globalStream); + setTimeout(() => { + play_recording_signal(); + start_recording(mediaRecorder); + setTimeout(() => { + mediaRecorder.stop(); + play_recording_signal(); + }, duration_ms); + }, recording_signal_duration_ms + buffer * 1000); + return () => { + if (recorded_sound === undefined) { + throw new Error('recording still being processed'); + } else { + return recorded_sound; + } + }; +} + +// ============================================================================= +// Module's Exposed Functions +// +// This file only includes the implementation and documentation of exposed +// functions of the module. For private functions dealing with the browser's +// graphics library context, see './webGL_curves.ts'. +// ============================================================================= + +// Core functions + +/** + * Makes a Stereo Sound with given wave function and duration. + * The wave function is a function: number -> number + * that takes in a non-negative input time and returns an amplitude + * between -1 and 1. + * + * @param left_wave wave function of the left channel of the sound + * @param right_wave wave function of the right channel of the sound + * @param duration duration of the sound + * @return resulting stereo sound + * @example const s = make_stereo_sound(t => Math_sin(2 * Math_PI * 440 * t), t => Math_sin(2 * Math_PI * 300 * t), 5); + */ +export function make_stereo_sound( + left_wave: Wave, + right_wave: Wave, + duration: number, +): Sound { + return pair( + pair( + (t: number) => (t >= duration ? 0 : left_wave(t)), + (t: number) => (t >= duration ? 0 : right_wave(t)), + ), + duration, + ); +} + +/** + * Makes a Sound with given wave function and duration. + * The wave function is a function: number -> number + * that takes in a non-negative input time and returns an amplitude + * between -1 and 1. + * + * @param wave wave function of the sound + * @param duration duration of the sound + * @return with wave as wave function and duration as duration + * @example const s = make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5); + */ +export function make_sound(wave: Wave, duration: number): Sound { + return make_stereo_sound(wave, wave, duration); +} + +/** + * Accesses the left wave function of a given Sound. + * + * @param sound given Sound + * @return the wave function of the Sound + * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + */ +export function get_left_wave(sound: Sound): Wave { + return head(head(sound)); +} + +/** + * Accesses the left wave function of a given Sound. + * + * @param sound given Sound + * @return the wave function of the Sound + * @example get_wave(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns t => Math_sin(2 * Math_PI * 440 * t) + */ +export function get_right_wave(sound: Sound): Wave { + return tail(head(sound)); +} + +/** + * Accesses the duration of a given Sound. + * + * @param sound given Sound + * @return the duration of the Sound + * @example get_duration(make_sound(t => Math_sin(2 * Math_PI * 440 * t), 5)); // Returns 5 + */ +export function get_duration(sound: Sound): number { + return tail(sound); +} + +/** + * Checks if the argument is a Sound + * + * @param x input to be checked + * @return true if x is a Sound, false otherwise + * @example is_sound(make_sound(t => 0, 2)); // Returns true + */ +export function is_sound(x: any): boolean { + return ( + is_pair(x) + && typeof get_left_wave(x) === 'function' + && typeof get_right_wave(x) === 'function' + && typeof get_duration(x) === 'number' + ); +} + +/** + * Plays the given Wave using the computer’s sound device, for the duration + * given in seconds. + * The sound is only played if no other sounds are currently being played. + * + * @param wave the wave function to play, starting at 0 + * @return the given sound + * @example play_wave(t => math_sin(t * 3000), 5); + */ +export function play_wave(wave: Wave, duration: number): AudioPlayed { + return play(make_sound(wave, duration)); +} + +/** + * Plays the given two Waves using the computer’s sound device, for the duration + * given in seconds. The first Wave is for the left channel, the second for the + * right channel. + * The sound is only played if no other sounds are currently being played. + * + * @param wave1 the wave function to play on the left channel, starting at 0 + * @param wave2 the wave function to play on the right channel, starting at 0 + * @return the given sound + * @example play_waves(t => math_sin(t * 3000), t => math_sin(t * 6000), 5); + */ +export function play_waves( + wave1: Wave, + wave2: Wave, + duration: number, +): AudioPlayed { + return play(make_stereo_sound(wave1, wave2, duration)); +} + +/** + * Plays the given Sound using the computer’s sound device. + * The sound is only played if no other sounds are currently being played. + * + * @param sound the sound to play + * @return the given sound + * @example play(sine_sound(440, 5)); + */ +export function play(sound: Sound): AudioPlayed { + // Type-check sound + if (!is_sound(sound)) { + throw new Error(`play is expecting sound, but encountered ${sound}`); + // If a sound is already playing, terminate execution. + } else if (isPlaying) { + throw new Error('play: audio system still playing previous sound'); + } else if (get_duration(sound) < 0) { + throw new Error('play: duration of sound is negative'); + } else { + // Instantiate audio context if it has not been instantiated. + if (!audioplayer) { + init_audioCtx(); + } + + const channel: number[] = []; + const len = Math.ceil(FS * get_duration(sound)); + + let Ltemp: number; + let Rtemp: number; + let Lprev_value = 0; + let Rprev_value = 0; + + const left_wave = get_left_wave(sound); + const right_wave = get_right_wave(sound); + for (let i = 0; i < len; i += 1) { + Ltemp = left_wave(i / FS); + // clip amplitude + if (Ltemp > 1) { + channel[2 * i] = 1; + } else if (Ltemp < -1) { + channel[2 * i] = -1; + } else { + channel[2 * i] = Ltemp; + } + + // smoothen out sudden cut-outs + if ( + channel[2 * i] === 0 + && Math.abs(channel[2 * i] - Lprev_value) > 0.01 + ) { + channel[2 * i] = Lprev_value * 0.999; + } + + Lprev_value = channel[2 * i]; + + Rtemp = right_wave(i / FS); + // clip amplitude + if (Rtemp > 1) { + channel[2 * i + 1] = 1; + } else if (Rtemp < -1) { + channel[2 * i + 1] = -1; + } else { + channel[2 * i + 1] = Rtemp; + } + + // smoothen out sudden cut-outs + if ( + channel[2 * i + 1] === 0 + && Math.abs(channel[2 * i] - Rprev_value) > 0.01 + ) { + channel[2 * i + 1] = Rprev_value * 0.999; + } + + Rprev_value = channel[2 * i + 1]; + } + + // quantize + for (let i = 0; i < channel.length; i += 1) { + channel[i] = Math.floor(channel[i] * 32767.999); + } + + const riffwave = new RIFFWAVE([]); + riffwave.header.sampleRate = FS; + riffwave.header.numChannels = 2; + riffwave.header.bitsPerSample = 16; + riffwave.Make(channel); + + /* + const audio = new Audio(riffwave.dataURI); + const source2 = audioplayer.createMediaElementSource(audio); + source2.connect(audioplayer.destination); + + // Connect data to output destination + isPlaying = true; + audio.play(); + audio.onended = () => { + source2.disconnect(audioplayer.destination); + isPlaying = false; + }; */ + + const audio = { + toReplString: () => '', + dataUri: riffwave.dataURI, + }; + + audioPlayed.push(audio); + return audio; + } +} + +/** + * Plays the given Sound using the computer’s sound device + * on top of any sounds that are currently playing. + * + * @param sound the sound to play + * @example play_concurrently(sine_sound(440, 5)); + */ +export function play_concurrently(sound: Sound): void { + // Type-check sound + if (!is_sound(sound)) { + throw new Error( + `play_concurrently is expecting sound, but encountered ${sound}`, + ); + } else if (get_duration(sound) <= 0) { + // Do nothing + } else { + // Instantiate audio context if it has not been instantiated. + if (!audioplayer) { + init_audioCtx(); + } + + const channel: number[] = Array[2 * Math.ceil(FS * get_duration(sound))]; + + let Ltemp: number; + let Rtemp: number; + let prev_value = 0; + + const left_wave = get_left_wave(sound); + + for (let i = 0; i < channel.length; i += 2) { + Ltemp = left_wave(i / FS); + // clip amplitude + if (Ltemp > 1) { + channel[i] = 1; + } else if (Ltemp < -1) { + channel[i] = -1; + } else { + channel[i] = Ltemp; + } + + // smoothen out sudden cut-outs + if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { + channel[i] = prev_value * 0.999; + } + + prev_value = channel[i]; + } + + prev_value = 0; + const right_wave = get_right_wave(sound); + for (let i = 1; i < channel.length; i += 2) { + Rtemp = right_wave(i / FS); + // clip amplitude + if (Rtemp > 1) { + channel[i] = 1; + } else if (Rtemp < -1) { + channel[i] = -1; + } else { + channel[i] = Rtemp; + } + + // smoothen out sudden cut-outs + if (channel[i] === 0 && Math.abs(channel[i] - prev_value) > 0.01) { + channel[i] = prev_value * 0.999; + } + + prev_value = channel[i]; + } + + // quantize + for (let i = 0; i < channel.length; i += 1) { + channel[i] = Math.floor(channel[i] * 32767.999); + } + + const riffwave = new RIFFWAVE([]); + riffwave.header.sampleRate = FS; + riffwave.header.numChannels = 2; + riffwave.header.bitsPerSample = 16; + riffwave.Make(channel); + const audio = new Audio(riffwave.dataURI); + const source2 = audioplayer.createMediaElementSource(audio); + source2.connect(audioplayer.destination); + + // Connect data to output destination + audio.play(); + isPlaying = true; + audio.onended = () => { + source2.disconnect(audioplayer.destination); + isPlaying = false; + }; + } +} + +/** + * Stops all currently playing sounds. + */ +export function stop(): void { + audioplayer.close(); + isPlaying = false; +} + +// Stereo only functions + +/** + * Centers a Sound by averaging its left and right channels, + * resulting in an effectively mono sound. + * + * @param sound the sound to be squashed + * @return a new sound with the left and right channels averaged + */ +export function squash(sound: Sound): Sound { + const left = get_left_wave(sound); + const right = get_right_wave(sound); + return make_sound((t) => 0.5 * (left(t) + right(t)), get_duration(sound)); +} + +/** + * Returns a Sound Transformer that pans a sound based on the pan amount. + * The input sound is first squashed to mono. + * An amount of `-1` is a hard left pan, `0` is balanced, `1` is hard right pan. + * + * @param amount the pan amount, from -1 to 1 + * @return a Sound Transformer that pans a Sound + */ +export function pan(amount: number): SoundTransformer { + return (sound) => { + if (amount > 1) { + // eslint-disable-next-line no-param-reassign + amount = 1; + } + if (amount < -1) { + // eslint-disable-next-line no-param-reassign + amount = -1; + } + // eslint-disable-next-line no-param-reassign + sound = squash(sound); + return make_stereo_sound( + (t) => ((1 - amount) / 2) * get_left_wave(sound)(t), + (t) => ((1 + amount) / 2) * get_right_wave(sound)(t), + get_duration(sound), + ); + }; +} + +/** + * Returns a Sound Transformer that uses a Sound to pan another Sound. + * The modulator is treated as a mono sound and its output is used to pan + * an input Sound. + * `-1` is a hard left pan, `0` is balanced, `1` is hard right pan. + * + * @param modulator the Sound used to modulate the pan of another sound + * @return a Sound Transformer that pans a Sound + */ +export function pan_mod(modulator: Sound): SoundTransformer { + const amount = (t: number) => { + let output = get_left_wave(modulator)(t) + get_right_wave(modulator)(t); + if (output > 1) { + output = 1; + } + if (output < -1) { + output = -1; + } + return output; + }; + return (sound) => { + // eslint-disable-next-line no-param-reassign + sound = squash(sound); + return make_stereo_sound( + (t) => ((1 - amount(t)) / 2) * get_left_wave(sound)(t), + (t) => ((1 + amount(t)) / 2) * get_right_wave(sound)(t), + get_duration(sound), + ); + }; +} + +// Primitive sounds + +/** + * Makes a noise sound with given duration + * + * @param duration the duration of the noise sound + * @return resulting noise sound + * @example noise_sound(5); + */ +export function noise_sound(duration: number): Sound { + return make_sound((t) => Math.random() * 2 - 1, duration); +} + +/** + * Makes a silence sound with given duration + * + * @param duration the duration of the silence sound + * @return resulting silence sound + * @example silence_sound(5); + */ +export function silence_sound(duration: number): Sound { + return make_sound((t) => 0, duration); +} + +/** + * Makes a sine wave sound with given frequency and duration + * + * @param freq the frequency of the sine wave sound + * @param duration the duration of the sine wave sound + * @return resulting sine wave sound + * @example sine_sound(440, 5); + */ +export function sine_sound(freq: number, duration: number): Sound { + return make_sound((t) => Math.sin(2 * Math.PI * t * freq), duration); +} + +/** + * Makes a square wave sound with given frequency and duration + * + * @param freq the frequency of the square wave sound + * @param duration the duration of the square wave sound + * @return resulting square wave sound + * @example square_sound(440, 5); + */ +export function square_sound(f: number, duration: number): Sound { + function fourier_expansion_square(t: number) { + let answer = 0; + for (let i = 1; i <= fourier_expansion_level; i += 1) { + answer += Math.sin(2 * Math.PI * (2 * i - 1) * f * t) / (2 * i - 1); + } + return answer; + } + return make_sound( + (t) => (4 / Math.PI) * fourier_expansion_square(t), + duration, + ); +} + +/** + * Makes a triangle wave sound with given frequency and duration + * + * @param freq the frequency of the triangle wave sound + * @param duration the duration of the triangle wave sound + * @return resulting triangle wave sound + * @example triangle_sound(440, 5); + */ +export function triangle_sound(freq: number, duration: number): Sound { + function fourier_expansion_triangle(t: number) { + let answer = 0; + for (let i = 0; i < fourier_expansion_level; i += 1) { + answer + += ((-1) ** i * Math.sin((2 * i + 1) * t * freq * Math.PI * 2)) + / (2 * i + 1) ** 2; + } + return answer; + } + return make_sound( + (t) => (8 / Math.PI / Math.PI) * fourier_expansion_triangle(t), + duration, + ); +} + +/** + * Makes a sawtooth wave sound with given frequency and duration + * + * @param freq the frequency of the sawtooth wave sound + * @param duration the duration of the sawtooth wave sound + * @return resulting sawtooth wave sound + * @example sawtooth_sound(440, 5); + */ +export function sawtooth_sound(freq: number, duration: number): Sound { + function fourier_expansion_sawtooth(t: number) { + let answer = 0; + for (let i = 1; i <= fourier_expansion_level; i += 1) { + answer += Math.sin(2 * Math.PI * i * freq * t) / i; + } + return answer; + } + return make_sound( + (t) => 1 / 2 - (1 / Math.PI) * fourier_expansion_sawtooth(t), + duration, + ); +} + +// Composition Operators + +/** + * Makes a new Sound by combining the sounds in a given list + * where the second sound is appended to the end of the first sound, + * the third sound is appended to the end of the second sound, and + * so on. The effect is that the sounds in the list are joined end-to-end + * + * @param list_of_sounds given list of sounds + * @return the combined Sound + * @example consecutively(list(sine_sound(200, 2), sine_sound(400, 3))); + */ +export function consecutively(list_of_sounds: List): Sound { + function stereo_cons_two(sound1: Sound, sound2: Sound) { + const Lwave1 = get_left_wave(sound1); + const Rwave1 = get_right_wave(sound1); + const Lwave2 = get_left_wave(sound2); + const Rwave2 = get_right_wave(sound2); + const dur1 = get_duration(sound1); + const dur2 = get_duration(sound2); + const new_left = (t: number) => (t < dur1 ? Lwave1(t) : Lwave2(t - dur1)); + const new_right = (t: number) => (t < dur1 ? Rwave1(t) : Rwave2(t - dur1)); + return make_stereo_sound(new_left, new_right, dur1 + dur2); + } + return accumulate(stereo_cons_two, silence_sound(0), list_of_sounds); +} + +/** + * Makes a new Sound by combining the sounds in a given list + * where all the sounds are overlapped on top of each other. + * + * @param list_of_sounds given list of sounds + * @return the combined Sound + * @example simultaneously(list(sine_sound(200, 2), sine_sound(400, 3))) + */ +export function simultaneously(list_of_sounds: List): Sound { + function stereo_simul_two(sound1: Sound, sound2: Sound) { + const Lwave1 = get_left_wave(sound1); + const Rwave1 = get_right_wave(sound1); + const Lwave2 = get_left_wave(sound2); + const Rwave2 = get_right_wave(sound2); + const dur1 = get_duration(sound1); + const dur2 = get_duration(sound2); + const new_left = (t: number) => Lwave1(t) + Lwave2(t); + const new_right = (t: number) => Rwave1(t) + Rwave2(t); + const new_dur = dur1 < dur2 ? dur2 : dur1; + return make_stereo_sound(new_left, new_right, new_dur); + } + + const unnormed = accumulate( + stereo_simul_two, + silence_sound(0), + list_of_sounds, + ); + const sounds_length = length(list_of_sounds); + const normalised_left = (t: number) => head(head(unnormed))(t) / sounds_length; + const normalised_right = (t: number) => tail(head(unnormed))(t) / sounds_length; + const highest_duration = tail(unnormed); + return make_stereo_sound(normalised_left, normalised_right, highest_duration); +} + +/** + * Returns an envelope: a function from Sound to Sound. + * When the adsr envelope is applied to a Sound, it returns + * a new Sound with its amplitude modified according to parameters + * The relative amplitude increases from 0 to 1 linearly over the + * attack proportion, then decreases from 1 to sustain level over the + * decay proportion, and remains at that level until the release + * proportion when it decays back to 0. + * @param attack_ratio proportion of Sound in attack phase + * @param decay_ratio proportion of Sound decay phase + * @param sustain_level sustain level between 0 and 1 + * @param release_ratio proportion of Sound in release phase + * @return Envelope a function from Sound to Sound + * @example adsr(0.2, 0.3, 0.3, 0.1)(sound); + */ +export function adsr( + attack_ratio: number, + decay_ratio: number, + sustain_level: number, + release_ratio: number, +): SoundTransformer { + return (sound) => { + const Lwave = get_left_wave(sound); + const Rwave = get_right_wave(sound); + const duration = get_duration(sound); + const attack_time = duration * attack_ratio; + const decay_time = duration * decay_ratio; + const release_time = duration * release_ratio; + + function adsrHelper(wave: Wave) { + return (x: number) => { + if (x < attack_time) { + return wave(x) * (x / attack_time); + } + if (x < attack_time + decay_time) { + return ( + ((1 - sustain_level) * linear_decay(decay_time)(x - attack_time) + + sustain_level) + * wave(x) + ); + } + if (x < duration - release_time) { + return wave(x) * sustain_level; + } + return ( + wave(x) + * sustain_level + * linear_decay(release_time)(x - (duration - release_time)) + ); + }; + } + return make_stereo_sound(adsrHelper(Lwave), adsrHelper(Rwave), duration); + }; +} + +/** + * Returns a Sound that results from applying a list of envelopes + * to a given wave form. The wave form is a Sound generator that + * takes a frequency and a duration as arguments and produces a + * Sound with the given frequency and duration. Each envelope is + * applied to a harmonic: the first harmonic has the given frequency, + * the second has twice the frequency, the third three times the + * frequency etc. The harmonics are then layered simultaneously to + * produce the resulting Sound. + * @param waveform function from pair(frequency, duration) to Sound + * @param base_frequency frequency of the first harmonic + * @param duration duration of the produced Sound, in seconds + * @param envelopes – list of envelopes, which are functions from Sound to Sound + * @return Sound resulting Sound + * @example stacking_adsr(sine_sound, 300, 5, list(adsr(0.1, 0.3, 0.2, 0.5), adsr(0.2, 0.5, 0.6, 0.1), adsr(0.3, 0.1, 0.7, 0.3))); + */ +export function stacking_adsr( + waveform: SoundProducer, + base_frequency: number, + duration: number, + envelopes: List, +): Sound { + function zip(lst: List, n: number) { + if (is_null(lst)) { + return lst; + } + return pair(pair(n, head(lst)), zip(tail(lst), n + 1)); + } + + return simultaneously( + accumulate( + (x: any, y: any) => pair(tail(x)(waveform(base_frequency * head(x), duration)), y), + null, + zip(envelopes, 1), + ), + ); +} + +/** + * Returns a SoundTransformer which uses its argument + * to modulate the phase of a (carrier) sine wave + * of given frequency and duration with a given Sound. + * Modulating with a low frequency Sound results in a vibrato effect. + * Modulating with a Sound with frequencies comparable to + * the sine wave frequency results in more complex wave forms. + * + * @param freq the frequency of the sine wave to be modulated + * @param duration the duration of the output soud + * @param amount the amount of modulation to apply to the carrier sine wave + * @return function which takes in a Sound and returns a Sound + * @example phase_mod(440, 5, 1)(sine_sound(220, 5)); + */ +export function phase_mod( + freq: number, + duration: number, + amount: number, +): SoundTransformer { + return (modulator: Sound) => make_stereo_sound( + (t) => Math.sin(2 * Math.PI * t * freq + amount * get_left_wave(modulator)(t)), + (t) => Math.sin( + 2 * Math.PI * t * freq + amount * get_right_wave(modulator)(t), + ), + duration, + ); +} + +// MIDI conversion functions + +/** + * Converts a letter name to its corresponding MIDI note. + * The letter name is represented in standard pitch notation. + * Examples are "A5", "Db3", "C#7". + * Refer to
    this mapping from + * letter name to midi notes. + * + * @param letter_name given letter name + * @return the corresponding midi note + * @example letter_name_to_midi_note("C4"); // Returns 60 + */ +export function letter_name_to_midi_note(note: string): number { + let res = 12; // C0 is midi note 12 + const n = note[0].toUpperCase(); + switch (n) { + case 'D': + res += 2; + break; + + case 'E': + res += 4; + break; + + case 'F': + res += 5; + break; + + case 'G': + res += 7; + break; + + case 'A': + res += 9; + break; + + case 'B': + res += 11; + break; + + default: + break; + } + + if (note.length === 2) { + res += parseInt(note[1]) * 12; + } else if (note.length === 3) { + switch (note[1]) { + case '#': + res += 1; + break; + + case 'b': + res -= 1; + break; + + default: + break; + } + res += parseInt(note[2]) * 12; + } + return res; +} + +/** + * Converts a MIDI note to its corresponding frequency. + * + * @param note given MIDI note + * @return the frequency of the MIDI note + * @example midi_note_to_frequency(69); // Returns 440 + */ +export function midi_note_to_frequency(note: number): number { + // A4 = 440Hz = midi note 69 + return 440 * 2 ** ((note - 69) / 12); +} + +/** + * Converts a letter name to its corresponding frequency. + * + * @param letter_name given letter name + * @return the corresponding frequency + * @example letter_name_to_frequency("A4"); // Returns 440 + */ +export function letter_name_to_frequency(note: string): number { + return midi_note_to_frequency(letter_name_to_midi_note(note)); +} + +// Instruments + +/** + * returns a Sound reminiscent of a bell, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting bell Sound with given pitch and duration + * @example bell(40, 1); + */ +export function bell(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list( + adsr(0, 0.6, 0, 0.05), + adsr(0, 0.6618, 0, 0.05), + adsr(0, 0.7618, 0, 0.05), + adsr(0, 0.9071, 0, 0.05), + ), + ); +} + +/** + * returns a Sound reminiscent of a cello, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting cello Sound with given pitch and duration + * @example cello(36, 5); + */ +export function cello(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0.05, 0, 1, 0.1), adsr(0.05, 0, 1, 0.15), adsr(0, 0, 0.2, 0.15)), + ); +} + +/** + * returns a Sound reminiscent of a piano, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting piano Sound with given pitch and duration + * @example piano(48, 5); + */ +export function piano(note: number, duration: number): Sound { + return stacking_adsr( + triangle_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0, 0.515, 0, 0.05), adsr(0, 0.32, 0, 0.05), adsr(0, 0.2, 0, 0.05)), + ); +} + +/** + * returns a Sound reminiscent of a trombone, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting trombone Sound with given pitch and duration + * @example trombone(60, 2); + */ +export function trombone(note: number, duration: number): Sound { + return stacking_adsr( + square_sound, + midi_note_to_frequency(note), + duration, + list(adsr(0.2, 0, 1, 0.1), adsr(0.3236, 0.6, 0, 0.1)), + ); +} + +/** + * returns a Sound reminiscent of a violin, playing + * a given note for a given duration + * @param note MIDI note + * @param duration duration in seconds + * @return Sound resulting violin Sound with given pitch and duration + * @example violin(53, 4); + */ +export function violin(note: number, duration: number): Sound { + return stacking_adsr( + sawtooth_sound, + midi_note_to_frequency(note), + duration, + list( + adsr(0.35, 0, 1, 0.15), + adsr(0.35, 0, 1, 0.15), + adsr(0.45, 0, 1, 0.15), + adsr(0.45, 0, 1, 0.15), + ), + ); +} diff --git a/src/bundles/stereo_sound/index.ts b/src/bundles/stereo_sound/index.ts index 720d4bd94..7f5c1ed59 100644 --- a/src/bundles/stereo_sound/index.ts +++ b/src/bundles/stereo_sound/index.ts @@ -1,116 +1,116 @@ -import { ModuleContext } from 'js-slang'; -import { - // Constructor/Accessors/Typecheck - make_stereo_sound, - make_sound, - get_left_wave, - get_right_wave, - get_duration, - is_sound, - squash, - pan, - pan_mod, - // Play-related - play, - play_wave, - play_waves, - play_concurrently, - stop, - // Recording - init_record, - record, - record_for, - // Composition and Envelopes - consecutively, - simultaneously, - phase_mod, - adsr, - stacking_adsr, - // Basic waveforms - noise_sound, - silence_sound, - sine_sound, - sawtooth_sound, - triangle_sound, - square_sound, - // MIDI - letter_name_to_midi_note, - midi_note_to_frequency, - letter_name_to_frequency, - // Instruments - bell, - cello, - piano, - trombone, - violin, - audioPlayed, -} from './functions'; -import { StereoSoundsModuleState } from './types'; - -export default function sounds(params, contexts: Map) { - // Update the module's global context - let moduleContext = contexts.get('stereo_sound'); - - if (!moduleContext) { - moduleContext = { - tabs: [], - state: { - audioPlayed, - }, - }; - - contexts.set('stereo_sound', moduleContext); - } else if (!moduleContext.state) { - moduleContext.state = { - audioPlayed, - }; - } else { - (moduleContext.state as StereoSoundsModuleState).audioPlayed = audioPlayed; - } - - return { - // Constructor/Accessors/Typecheck - make_stereo_sound, - make_sound, - get_left_wave, - get_right_wave, - get_duration, - is_sound, - squash, - pan, - pan_mod, - // Play-related - play, - play_wave, - play_waves, - play_concurrently, - stop, - // Recording - init_record, - record, - record_for, - // Composition and Envelopes - consecutively, - simultaneously, - phase_mod, - adsr, - stacking_adsr, - // Basic waveforms - noise_sound, - silence_sound, - sine_sound, - sawtooth_sound, - triangle_sound, - square_sound, - // MIDI - letter_name_to_midi_note, - midi_note_to_frequency, - letter_name_to_frequency, - // Instruments - bell, - cello, - piano, - trombone, - violin, - }; -} +import { ModuleContext } from 'js-slang'; +import { + // Constructor/Accessors/Typecheck + make_stereo_sound, + make_sound, + get_left_wave, + get_right_wave, + get_duration, + is_sound, + squash, + pan, + pan_mod, + // Play-related + play, + play_wave, + play_waves, + play_concurrently, + stop, + // Recording + init_record, + record, + record_for, + // Composition and Envelopes + consecutively, + simultaneously, + phase_mod, + adsr, + stacking_adsr, + // Basic waveforms + noise_sound, + silence_sound, + sine_sound, + sawtooth_sound, + triangle_sound, + square_sound, + // MIDI + letter_name_to_midi_note, + midi_note_to_frequency, + letter_name_to_frequency, + // Instruments + bell, + cello, + piano, + trombone, + violin, + audioPlayed, +} from './functions'; +import { StereoSoundsModuleState } from './types'; + +export default function sounds(params, contexts: Map) { + // Update the module's global context + let moduleContext = contexts.get('stereo_sound'); + + if (!moduleContext) { + moduleContext = { + tabs: [], + state: { + audioPlayed, + }, + }; + + contexts.set('stereo_sound', moduleContext); + } else if (!moduleContext.state) { + moduleContext.state = { + audioPlayed, + }; + } else { + (moduleContext.state as StereoSoundsModuleState).audioPlayed = audioPlayed; + } + + return { + // Constructor/Accessors/Typecheck + make_stereo_sound, + make_sound, + get_left_wave, + get_right_wave, + get_duration, + is_sound, + squash, + pan, + pan_mod, + // Play-related + play, + play_wave, + play_waves, + play_concurrently, + stop, + // Recording + init_record, + record, + record_for, + // Composition and Envelopes + consecutively, + simultaneously, + phase_mod, + adsr, + stacking_adsr, + // Basic waveforms + noise_sound, + silence_sound, + sine_sound, + sawtooth_sound, + triangle_sound, + square_sound, + // MIDI + letter_name_to_midi_note, + midi_note_to_frequency, + letter_name_to_frequency, + // Instruments + bell, + cello, + piano, + trombone, + violin, + }; +} diff --git a/src/bundles/stereo_sound/list.ts b/src/bundles/stereo_sound/list.ts index e5205937c..d2ae0927d 100644 --- a/src/bundles/stereo_sound/list.ts +++ b/src/bundles/stereo_sound/list.ts @@ -1,372 +1,372 @@ -/* eslint-disable @typescript-eslint/naming-convention, no-else-return, prefer-template, no-param-reassign, no-plusplus, operator-assignment, no-lonely-if */ -/* prettier-ignore */ -// list.js: Supporting lists in the Scheme style, using pairs made -// up of two-element JavaScript array (vector) - -// Author: Martin Henz - -// Note: this library is used in the externalLibs of cadet-frontend. -// It is distinct from the LISTS library of Source §2, which contains -// primitive and predeclared functions from Chapter 2 of SICP JS. - -// array test works differently for Rhino and -// the Firefox environment (especially Web Console) -export function array_test(x) : boolean { - if (Array.isArray === undefined) { - return x instanceof Array - } else { - return Array.isArray(x) - } -} - -// pair constructs a pair using a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function pair(x, xs): [any, any] { - return [x, xs]; -} - -// is_pair returns true iff arg is a two-element array -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_pair(x): boolean { - return array_test(x) && x.length === 2; -} - -// head returns the first component of the given pair, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function head(xs): any { - if (is_pair(xs)) { - return xs[0]; - } else { - throw new Error( - 'head(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// tail returns the second component of the given pair -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function tail(xs) { - if (is_pair(xs)) { - return xs[1]; - } else { - throw new Error( - 'tail(xs) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// is_null returns true if arg is exactly null -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_null(xs) { - return xs === null; -} - -// is_list recurses down the list and checks that it ends with the empty list [] -// does not throw Value exceptions -// LOW-LEVEL FUNCTION, NOT SOURCE -export function is_list(xs) { - for (; ; xs = tail(xs)) { - if (is_null(xs)) { - return true; - } else if (!is_pair(xs)) { - return false; - } - } -} - -// list makes a list out of its arguments -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list(...args) { - let the_list: any = null; - for (let i = args.length - 1; i >= 0; i--) { - the_list = pair(args[i], the_list); - } - return the_list; -} - -// list_to_vector returns vector that contains the elements of the argument list -// in the given order. -// list_to_vector throws an exception if the argument is not a list -// LOW-LEVEL FUNCTION, NOT SOURCE -export function list_to_vector(lst) { - const vector: any[] = []; - while (!is_null(lst)) { - vector.push(head(lst)); - lst = tail(lst); - } - return vector; -} - -// vector_to_list returns a list that contains the elements of the argument vector -// in the given order. -// vector_to_list throws an exception if the argument is not a vector -// LOW-LEVEL FUNCTION, NOT SOURCE -export function vector_to_list(vector) { - let result: any = null; - for (let i = vector.length - 1; i >= 0; i = i - 1) { - result = pair(vector[i], result); - } - return result; -} - -// returns the length of a given argument list -// throws an exception if the argument is not a list -export function length(xs) { - let i = 0; - while (!is_null(xs)) { - i += 1; - xs = tail(xs); - } - return i; -} - -// map applies first arg f to the elements of the second argument, -// assumed to be a list. -// f is applied element-by-element: -// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] -// map throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the first -// argument is not a function. -// tslint:disable-next-line:ban-types -export function map(f, xs) { - return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); -} - -// build_list takes a non-negative integer n as first argument, -// and a function fun as second argument. -// build_list returns a list of n elements, that results from -// applying fun to the numbers from 0 to n-1. -// tslint:disable-next-line:ban-types -export function build_list(n, fun) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'build_list(n, fun) expects a positive integer as ' + - 'argument n, but encountered ' + - n - ); - } - - // tslint:disable-next-line:ban-types - function build(i, alreadyBuilt) { - if (i < 0) { - return alreadyBuilt; - } else { - return build(i - 1, pair(fun(i), alreadyBuilt)); - } - } - - return build(n - 1, null); -} - -// for_each applies first arg fun to the elements of the list passed as -// second argument. fun is applied element-by-element: -// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). -// for_each returns true. -// for_each throws an exception if the second argument is not a list, -// and if the second argument is a non-empty list and the -// first argument is not a function. -// tslint:disable-next-line:ban-types -export function for_each(fun, xs) { - if (!is_list(xs)) { - throw new Error( - 'for_each expects a list as argument xs, but encountered ' + xs - ); - } - for (; !is_null(xs); xs = tail(xs)) { - fun(head(xs)); - } - return true; -} - -// reverse reverses the argument list -// reverse throws an exception if the argument is not a list. -export function reverse(xs) { - if (!is_list(xs)) { - throw new Error( - 'reverse(xs) expects a list as argument xs, but encountered ' + xs - ); - } - let result: any = null; - for (; !is_null(xs); xs = tail(xs)) { - result = pair(head(xs), result); - } - return result; -} - -// append first argument list and second argument list. -// In the result, the [] at the end of the first argument list -// is replaced by the second argument list -// append throws an exception if the first argument is not a list -export function append(xs, ys) { - if (is_null(xs)) { - return ys; - } else { - return pair(head(xs), append(tail(xs), ys)); - } -} - -// member looks for a given first-argument element in a given -// second argument list. It returns the first postfix sublist -// that starts with the given element. It returns [] if the -// element does not occur in the list -export function member(v, xs) { - for (; !is_null(xs); xs = tail(xs)) { - if (head(xs) === v) { - return xs; - } - } - return null; -} - -// removes the first occurrence of a given first-argument element -// in a given second-argument list. Returns the original list -// if there is no occurrence. -export function remove(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return tail(xs); - } else { - return pair(head(xs), remove(v, tail(xs))); - } - } -} - -// Similar to remove. But removes all instances of v instead of just the first -export function remove_all(v, xs) { - if (is_null(xs)) { - return null; - } else { - if (v === head(xs)) { - return remove_all(v, tail(xs)); - } else { - return pair(head(xs), remove_all(v, tail(xs))); - } - } -} - -// for backwards-compatibility -// equal computes the structural equality -// over its arguments -export function equal(item1, item2) { - if (is_pair(item1) && is_pair(item2)) { - return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); - } else { - return item1 === item2; - } -} - -// assoc treats the second argument as an association, -// a list of (index,value) pairs. -// assoc returns the first (index,value) pair whose -// index equal (using structural equality) to the given -// first argument v. Returns false if there is no such -// pair -export function assoc(v, xs) { - if (is_null(xs)) { - return false; - } else if (equal(v, head(head(xs)))) { - return head(xs); - } else { - return assoc(v, tail(xs)); - } -} - -// filter returns the sublist of elements of given list xs -// for which the given predicate function returns true. -// tslint:disable-next-line:ban-types -export function filter(pred, xs) { - if (is_null(xs)) { - return xs; - } else { - if (pred(head(xs))) { - return pair(head(xs), filter(pred, tail(xs))); - } else { - return filter(pred, tail(xs)); - } - } -} - -// enumerates numbers starting from start, -// using a step size of 1, until the number -// exceeds end. -export function enum_list(start, end) { - if (typeof start !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - start - ); - } - if (typeof end !== 'number') { - throw new Error( - 'enum_list(start, end) expects a number as argument start, but encountered ' + - end - ); - } - if (start > end) { - return null; - } else { - return pair(start, enum_list(start + 1, end)); - } -} - -// Returns the item in list lst at index n (the first item is at position 0) -export function list_ref(xs, n) { - if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { - throw new Error( - 'list_ref(xs, n) expects a positive integer as argument n, but encountered ' + - n - ); - } - for (; n > 0; --n) { - xs = tail(xs); - } - return head(xs); -} - -// accumulate applies given operation op to elements of a list -// in a right-to-left order, first apply op to the last element -// and an initial element, resulting in r1, then to the -// second-last element and r1, resulting in r2, etc, and finally -// to the first element and r_n-1, where n is the length of the -// list. -// accumulate(op,zero,list(1,2,3)) results in -// op(1, op(2, op(3, zero))) -export function accumulate(op, initial, sequence) { - if (is_null(sequence)) { - return initial; - } else { - return op(head(sequence), accumulate(op, initial, tail(sequence))); - } -} - -// set_head(xs,x) changes the head of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_head(xs, x) { - if (is_pair(xs)) { - xs[0] = x; - return undefined; - } else { - throw new Error( - 'set_head(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} - -// set_tail(xs,x) changes the tail of given pair xs to be x, -// throws an exception if the argument is not a pair -// LOW-LEVEL FUNCTION, NOT SOURCE -export function set_tail(xs, x) { - if (is_pair(xs)) { - xs[1] = x; - return undefined; - } else { - throw new Error( - 'set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs - ); - } -} +/* eslint-disable @typescript-eslint/naming-convention, no-else-return, prefer-template, no-param-reassign, no-plusplus, operator-assignment, no-lonely-if */ +/* prettier-ignore */ +// list.js: Supporting lists in the Scheme style, using pairs made +// up of two-element JavaScript array (vector) + +// Author: Martin Henz + +// Note: this library is used in the externalLibs of cadet-frontend. +// It is distinct from the LISTS library of Source §2, which contains +// primitive and predeclared functions from Chapter 2 of SICP JS. + +// array test works differently for Rhino and +// the Firefox environment (especially Web Console) +export function array_test(x) : boolean { + if (Array.isArray === undefined) { + return x instanceof Array; + } else { + return Array.isArray(x); + } +} + +// pair constructs a pair using a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function pair(x, xs): [any, any] { + return [x, xs]; +} + +// is_pair returns true iff arg is a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_pair(x): boolean { + return array_test(x) && x.length === 2; +} + +// head returns the first component of the given pair, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function head(xs): any { + if (is_pair(xs)) { + return xs[0]; + } else { + throw new Error( + 'head(xs) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// tail returns the second component of the given pair +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function tail(xs) { + if (is_pair(xs)) { + return xs[1]; + } else { + throw new Error( + 'tail(xs) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// is_null returns true if arg is exactly null +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_null(xs) { + return xs === null; +} + +// is_list recurses down the list and checks that it ends with the empty list [] +// does not throw Value exceptions +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_list(xs) { + for (; ; xs = tail(xs)) { + if (is_null(xs)) { + return true; + } else if (!is_pair(xs)) { + return false; + } + } +} + +// list makes a list out of its arguments +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list(...args) { + let the_list: any = null; + for (let i = args.length - 1; i >= 0; i--) { + the_list = pair(args[i], the_list); + } + return the_list; +} + +// list_to_vector returns vector that contains the elements of the argument list +// in the given order. +// list_to_vector throws an exception if the argument is not a list +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list_to_vector(lst) { + const vector: any[] = []; + while (!is_null(lst)) { + vector.push(head(lst)); + lst = tail(lst); + } + return vector; +} + +// vector_to_list returns a list that contains the elements of the argument vector +// in the given order. +// vector_to_list throws an exception if the argument is not a vector +// LOW-LEVEL FUNCTION, NOT SOURCE +export function vector_to_list(vector) { + let result: any = null; + for (let i = vector.length - 1; i >= 0; i = i - 1) { + result = pair(vector[i], result); + } + return result; +} + +// returns the length of a given argument list +// throws an exception if the argument is not a list +export function length(xs) { + let i = 0; + while (!is_null(xs)) { + i += 1; + xs = tail(xs); + } + return i; +} + +// map applies first arg f to the elements of the second argument, +// assumed to be a list. +// f is applied element-by-element: +// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] +// map throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the first +// argument is not a function. +// tslint:disable-next-line:ban-types +export function map(f, xs) { + return is_null(xs) ? null : pair(f(head(xs)), map(f, tail(xs))); +} + +// build_list takes a non-negative integer n as first argument, +// and a function fun as second argument. +// build_list returns a list of n elements, that results from +// applying fun to the numbers from 0 to n-1. +// tslint:disable-next-line:ban-types +export function build_list(n, fun) { + if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { + throw new Error( + 'build_list(n, fun) expects a positive integer as ' + + 'argument n, but encountered ' + + n, + ); + } + + // tslint:disable-next-line:ban-types + function build(i, alreadyBuilt) { + if (i < 0) { + return alreadyBuilt; + } else { + return build(i - 1, pair(fun(i), alreadyBuilt)); + } + } + + return build(n - 1, null); +} + +// for_each applies first arg fun to the elements of the list passed as +// second argument. fun is applied element-by-element: +// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). +// for_each returns true. +// for_each throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the +// first argument is not a function. +// tslint:disable-next-line:ban-types +export function for_each(fun, xs) { + if (!is_list(xs)) { + throw new Error( + 'for_each expects a list as argument xs, but encountered ' + xs, + ); + } + for (; !is_null(xs); xs = tail(xs)) { + fun(head(xs)); + } + return true; +} + +// reverse reverses the argument list +// reverse throws an exception if the argument is not a list. +export function reverse(xs) { + if (!is_list(xs)) { + throw new Error( + 'reverse(xs) expects a list as argument xs, but encountered ' + xs, + ); + } + let result: any = null; + for (; !is_null(xs); xs = tail(xs)) { + result = pair(head(xs), result); + } + return result; +} + +// append first argument list and second argument list. +// In the result, the [] at the end of the first argument list +// is replaced by the second argument list +// append throws an exception if the first argument is not a list +export function append(xs, ys) { + if (is_null(xs)) { + return ys; + } else { + return pair(head(xs), append(tail(xs), ys)); + } +} + +// member looks for a given first-argument element in a given +// second argument list. It returns the first postfix sublist +// that starts with the given element. It returns [] if the +// element does not occur in the list +export function member(v, xs) { + for (; !is_null(xs); xs = tail(xs)) { + if (head(xs) === v) { + return xs; + } + } + return null; +} + +// removes the first occurrence of a given first-argument element +// in a given second-argument list. Returns the original list +// if there is no occurrence. +export function remove(v, xs) { + if (is_null(xs)) { + return null; + } else { + if (v === head(xs)) { + return tail(xs); + } else { + return pair(head(xs), remove(v, tail(xs))); + } + } +} + +// Similar to remove. But removes all instances of v instead of just the first +export function remove_all(v, xs) { + if (is_null(xs)) { + return null; + } else { + if (v === head(xs)) { + return remove_all(v, tail(xs)); + } else { + return pair(head(xs), remove_all(v, tail(xs))); + } + } +} + +// for backwards-compatibility +// equal computes the structural equality +// over its arguments +export function equal(item1, item2) { + if (is_pair(item1) && is_pair(item2)) { + return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)); + } else { + return item1 === item2; + } +} + +// assoc treats the second argument as an association, +// a list of (index,value) pairs. +// assoc returns the first (index,value) pair whose +// index equal (using structural equality) to the given +// first argument v. Returns false if there is no such +// pair +export function assoc(v, xs) { + if (is_null(xs)) { + return false; + } else if (equal(v, head(head(xs)))) { + return head(xs); + } else { + return assoc(v, tail(xs)); + } +} + +// filter returns the sublist of elements of given list xs +// for which the given predicate function returns true. +// tslint:disable-next-line:ban-types +export function filter(pred, xs) { + if (is_null(xs)) { + return xs; + } else { + if (pred(head(xs))) { + return pair(head(xs), filter(pred, tail(xs))); + } else { + return filter(pred, tail(xs)); + } + } +} + +// enumerates numbers starting from start, +// using a step size of 1, until the number +// exceeds end. +export function enum_list(start, end) { + if (typeof start !== 'number') { + throw new Error( + 'enum_list(start, end) expects a number as argument start, but encountered ' + + start, + ); + } + if (typeof end !== 'number') { + throw new Error( + 'enum_list(start, end) expects a number as argument start, but encountered ' + + end, + ); + } + if (start > end) { + return null; + } else { + return pair(start, enum_list(start + 1, end)); + } +} + +// Returns the item in list lst at index n (the first item is at position 0) +export function list_ref(xs, n) { + if (typeof n !== 'number' || n < 0 || Math.floor(n) !== n) { + throw new Error( + 'list_ref(xs, n) expects a positive integer as argument n, but encountered ' + + n, + ); + } + for (; n > 0; --n) { + xs = tail(xs); + } + return head(xs); +} + +// accumulate applies given operation op to elements of a list +// in a right-to-left order, first apply op to the last element +// and an initial element, resulting in r1, then to the +// second-last element and r1, resulting in r2, etc, and finally +// to the first element and r_n-1, where n is the length of the +// list. +// accumulate(op,zero,list(1,2,3)) results in +// op(1, op(2, op(3, zero))) +export function accumulate(op, initial, sequence) { + if (is_null(sequence)) { + return initial; + } else { + return op(head(sequence), accumulate(op, initial, tail(sequence))); + } +} + +// set_head(xs,x) changes the head of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function set_head(xs, x) { + if (is_pair(xs)) { + xs[0] = x; + return undefined; + } else { + throw new Error( + 'set_head(xs,x) expects a pair as argument xs, but encountered ' + xs, + ); + } +} + +// set_tail(xs,x) changes the tail of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function set_tail(xs, x) { + if (is_pair(xs)) { + xs[1] = x; + return undefined; + } else { + throw new Error( + 'set_tail(xs,x) expects a pair as argument xs, but encountered ' + xs, + ); + } +} diff --git a/src/bundles/stereo_sound/riffwave.ts b/src/bundles/stereo_sound/riffwave.ts index 70a1af751..478588146 100644 --- a/src/bundles/stereo_sound/riffwave.ts +++ b/src/bundles/stereo_sound/riffwave.ts @@ -1,22 +1,22 @@ -/* - * RIFFWAVE.js v0.03 - Audio encoder for HTML5

    This is a visualiser for stop and copy garbage collector. Check the guide{' '} - + here . @@ -258,46 +258,54 @@ class CopyGC extends React.Component {

    {state.command}

    {state.description}

    - {state.leftDesc ? ( -
    - - {state.leftDesc} -
    - ) : ( - false - )} - {state.rightDesc ? ( -
    - - {state.rightDesc} -
    - ) : ( - false - )} + {state.leftDesc + ? ( +
    + + {state.leftDesc} +
    + ) + : ( + false + )} + {state.rightDesc + ? ( +
    + + {state.rightDesc} +
    + ) + : ( + false + )}

    Current step: {' '} - + {' '} {state.value} {' '} - +

    {

    {state.toSpace === 0 ? 'To Space' : 'From Space'}

    - {toMemoryMatrix && - toMemoryMatrix.length > 0 && - toMemoryMatrix.map((item, row) => ( -
    + {toMemoryMatrix + && toMemoryMatrix.length > 0 + && toMemoryMatrix.map((item, row) => ( +
    {row * state.column} - {item && - item.length > 0 && - item.map((content) => { + {item + && item.length > 0 + && item.map((content) => { const color = this.getMemoryColor(content); const bgColor = this.getBackgroundColor(content); return ( @@ -349,41 +360,48 @@ class CopyGC extends React.Component {

    {state.toSpace > 0 ? 'To Space' : 'From Space'}

    - {fromMemoryMatrix && - fromMemoryMatrix.length > 0 && - fromMemoryMatrix.map((item, row) => ( -
    + {fromMemoryMatrix + && fromMemoryMatrix.length > 0 + && fromMemoryMatrix.map((item, row) => ( +
    {row * state.column + state.memorySize / 2} {item && item.length > 0 ? item.map((content) => { - const color = this.getMemoryColor(content); - const bgColor = this.getBackgroundColor(content); - return ( -
    + - -
    - ); - }) + /> +
    + ); + }) : false}
    ))}
    -
    +
    {

    This is a visualiser for stop and copy garbage collector. Check the guide{' '} - + here . diff --git a/src/tabs/CopyGc/style.tsx b/src/tabs/CopyGc/style.tsx index aa890ff50..454735823 100644 --- a/src/tabs/CopyGc/style.tsx +++ b/src/tabs/CopyGc/style.tsx @@ -1,13 +1,13 @@ -export enum ThemeColor { - BLUE = 'lightblue', - PINK = 'salmon', - GREY = '#707070', - GREEN = '#42a870', - YELLOW = '#f0d60e', - RED = 'red', - BLACK = 'black', -} - -export const FONT = { - SMALL: 10, -}; +export enum ThemeColor { + BLUE = 'lightblue', + PINK = 'salmon', + GREY = '#707070', + GREEN = '#42a870', + YELLOW = '#f0d60e', + RED = 'red', + BLACK = 'black', +} + +export const FONT = { + SMALL: 10, +}; diff --git a/src/tabs/Csg/canvas_holder.tsx b/src/tabs/Csg/canvas_holder.tsx index d6fc926d8..841e05d63 100644 --- a/src/tabs/Csg/canvas_holder.tsx +++ b/src/tabs/Csg/canvas_holder.tsx @@ -1,117 +1,115 @@ -/* [Imports] */ -import { IconNames } from '@blueprintjs/icons'; -import React from 'react'; -import { - BP_BORDER_RADIUS, - BP_TAB_BUTTON_MARGIN, - BP_TAB_PANEL_MARGIN, -} from '../../bundles/csg/constants.js'; -import render from '../../bundles/csg/renderer'; -import HoverControlHint from './hover_control_hint'; -import { CanvasHolderProps, CanvasHolderState } from './types'; - -/* [Main] */ -export default class CanvasHolder extends React.Component< - CanvasHolderProps, - CanvasHolderState -> { - private canvasReference: React.RefObject = React.createRef(); - - // Called as part of the React lifecycle when this tab is created - public componentDidMount() { - let canvas: HTMLCanvasElement | null = this.canvasReference.current; - if (canvas === null) return; - - let getCurrentRequestId: () => number = render( - canvas, - this.props.moduleState - ); - - // Stops old render loop upon re-run to prevent regl context lost errors - canvas.addEventListener('webglcontextlost', () => - window.cancelAnimationFrame(getCurrentRequestId()) - ); - } - - // Only required method of a React Component. - // Returns a React Element created via JSX to instruct React to render a DOM - // node. - // Also attaches the canvasReference via the ref attribute, for imperatively - // modifying the canvas - public render() { - return ( -

    -
    - - - - - -
    - -
    - -
    -
    - ); - } -} +/* [Imports] */ +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; +import { + BP_BORDER_RADIUS, + BP_TAB_BUTTON_MARGIN, + BP_TAB_PANEL_MARGIN, +} from '../../bundles/csg/constants.js'; +import render from '../../bundles/csg/renderer'; +import HoverControlHint from './hover_control_hint'; +import { CanvasHolderProps, CanvasHolderState } from './types'; + +/* [Main] */ +export default class CanvasHolder extends React.Component< +CanvasHolderProps, +CanvasHolderState +> { + private canvasReference: React.RefObject = React.createRef(); + + // Called as part of the React lifecycle when this tab is created + public componentDidMount() { + let canvas: HTMLCanvasElement | null = this.canvasReference.current; + if (canvas === null) return; + + let getCurrentRequestId: () => number = render( + canvas, + this.props.moduleState, + ); + + // Stops old render loop upon re-run to prevent regl context lost errors + canvas.addEventListener('webglcontextlost', () => window.cancelAnimationFrame(getCurrentRequestId())); + } + + // Only required method of a React Component. + // Returns a React Element created via JSX to instruct React to render a DOM + // node. + // Also attaches the canvasReference via the ref attribute, for imperatively + // modifying the canvas + public render() { + return ( +
    +
    + + + + + +
    + +
    + +
    +
    + ); + } +} diff --git a/src/tabs/Csg/hover_control_hint.tsx b/src/tabs/Csg/hover_control_hint.tsx index d55ab6a68..818601be7 100644 --- a/src/tabs/Csg/hover_control_hint.tsx +++ b/src/tabs/Csg/hover_control_hint.tsx @@ -1,68 +1,68 @@ -/* [Imports] */ -import { Icon } from '@blueprintjs/core'; -import React from 'react'; -import { - BP_BORDER_RADIUS, - BP_ICON_COLOR, - BP_TOOLTIP_BACKGROUND_COLOR, - BP_TOOLTIP_PADDING, - BP_TOOLTIP_TEXT_COLOR, - SA_TAB_BUTTON_WIDTH, - SA_TAB_ICON_SIZE, -} from '../../bundles/csg/constants.js'; -import { HintProps, HintState } from './types'; - -/* [Main] */ - -// [CSS Values] -export default class HoverControlHint extends React.Component< - HintProps, - HintState -> { - public constructor(props: HintProps) { - super(props); - - this.state = { - showTooltip: false, - }; - } - - public render() { - return ( -
    this.setState({ showTooltip: true })} - onMouseLeave={() => this.setState({ showTooltip: false })} - > - - - {this.props.tooltipText} - -
    - ); - } -} +/* [Imports] */ +import { Icon } from '@blueprintjs/core'; +import React from 'react'; +import { + BP_BORDER_RADIUS, + BP_ICON_COLOR, + BP_TOOLTIP_BACKGROUND_COLOR, + BP_TOOLTIP_PADDING, + BP_TOOLTIP_TEXT_COLOR, + SA_TAB_BUTTON_WIDTH, + SA_TAB_ICON_SIZE, +} from '../../bundles/csg/constants.js'; +import { HintProps, HintState } from './types'; + +/* [Main] */ + +// [CSS Values] +export default class HoverControlHint extends React.Component< +HintProps, +HintState +> { + public constructor(props: HintProps) { + super(props); + + this.state = { + showTooltip: false, + }; + } + + public render() { + return ( +
    this.setState({ showTooltip: true })} + onMouseLeave={() => this.setState({ showTooltip: false })} + > + + + {this.props.tooltipText} + +
    + ); + } +} diff --git a/src/tabs/Csg/index.tsx b/src/tabs/Csg/index.tsx index e6ddc5871..6dfbe5858 100644 --- a/src/tabs/Csg/index.tsx +++ b/src/tabs/Csg/index.tsx @@ -1,45 +1,45 @@ -/* [Imports] */ -import { IconNames } from '@blueprintjs/icons'; -import { ModuleContext, ModuleState } from 'js-slang'; -import React, { ReactElement } from 'react'; -import { Core } from '../../bundles/csg/core.js'; -import { - CsgModuleState, - getModuleContext, - looseInstanceof, -} from '../../bundles/csg/utilities.js'; -import { DebuggerContext, ModuleContexts } from '../../typings/type_helpers'; -import CanvasHolder from './canvas_holder'; - -/* [Main] */ -export default { - // Called by the frontend to decide whether to spawn the CSG tab - toSpawn(_debuggerContext: DebuggerContext): boolean { - return Core.getRenderGroupManager().shouldRender(); - }, - - // Called by the frontend to know what to render in the CSG tab - body(debuggerContext: DebuggerContext): ReactElement { - let moduleContexts: ModuleContexts = debuggerContext.context.moduleContexts; - let potentialModuleContext: ModuleContext | null = getModuleContext( - moduleContexts - ); - if (potentialModuleContext === null) return
    ; - let moduleContext: ModuleContext = potentialModuleContext; - - let potentialModuleState: ModuleState | null | undefined = - moduleContext.state; - if (!looseInstanceof(potentialModuleState, CsgModuleState)) - return
    ; - let moduleState = potentialModuleState as CsgModuleState; - - Core.initialize(moduleState); - return ; - }, - - // BlueprintJS icon name - iconName: IconNames.SHAPES, - - // Icon tooltip in sidebar - label: 'CSG Tab', -}; +/* [Imports] */ +import { IconNames } from '@blueprintjs/icons'; +import { ModuleContext, ModuleState } from 'js-slang'; +import React, { ReactElement } from 'react'; +import { Core } from '../../bundles/csg/core.js'; +import { + CsgModuleState, + getModuleContext, + looseInstanceof, +} from '../../bundles/csg/utilities.js'; +import { DebuggerContext, ModuleContexts } from '../../typings/type_helpers'; +import CanvasHolder from './canvas_holder'; + +/* [Main] */ +export default { + // Called by the frontend to decide whether to spawn the CSG tab + toSpawn(_debuggerContext: DebuggerContext): boolean { + return Core.getRenderGroupManager() + .shouldRender(); + }, + + // Called by the frontend to know what to render in the CSG tab + body(debuggerContext: DebuggerContext): ReactElement { + let moduleContexts: ModuleContexts = debuggerContext.context.moduleContexts; + let potentialModuleContext: ModuleContext | null = getModuleContext( + moduleContexts, + ); + if (potentialModuleContext === null) return
    ; + let moduleContext: ModuleContext = potentialModuleContext; + + let potentialModuleState: ModuleState | null | undefined + = moduleContext.state; + if (!looseInstanceof(potentialModuleState, CsgModuleState)) { return
    ; } + let moduleState = potentialModuleState as CsgModuleState; + + Core.initialize(moduleState); + return ; + }, + + // BlueprintJS icon name + iconName: IconNames.SHAPES, + + // Icon tooltip in sidebar + label: 'CSG Tab', +}; diff --git a/src/tabs/Csg/types.ts b/src/tabs/Csg/types.ts index efce8a874..024d0cb64 100644 --- a/src/tabs/Csg/types.ts +++ b/src/tabs/Csg/types.ts @@ -1,24 +1,24 @@ -/* [Imports] */ -import { IconName } from '@blueprintjs/icons'; -import { CsgModuleState } from '../../bundles/csg/utilities.js'; - -/* [Exports] */ - -// React Component Props for the CSG canvas holder -export type CanvasHolderProps = { - moduleState: CsgModuleState; -}; - -// React Component State for the CSG canvas holder -export type CanvasHolderState = {}; - -// React Component Props for a control hint -export type HintProps = { - tooltipText: string; - iconName: IconName; -}; - -// React Component State for a control hint -export type HintState = { - showTooltip: boolean; -}; +/* [Imports] */ +import { IconName } from '@blueprintjs/icons'; +import { CsgModuleState } from '../../bundles/csg/utilities.js'; + +/* [Exports] */ + +// React Component Props for the CSG canvas holder +export type CanvasHolderProps = { + moduleState: CsgModuleState; +}; + +// React Component State for the CSG canvas holder +export type CanvasHolderState = {}; + +// React Component Props for a control hint +export type HintProps = { + tooltipText: string; + iconName: IconName; +}; + +// React Component State for a control hint +export type HintState = { + showTooltip: boolean; +}; diff --git a/src/tabs/Curve/3Dcurve_anim_canvas.tsx b/src/tabs/Curve/3Dcurve_anim_canvas.tsx index a88d5b310..5d7b07cc8 100644 --- a/src/tabs/Curve/3Dcurve_anim_canvas.tsx +++ b/src/tabs/Curve/3Dcurve_anim_canvas.tsx @@ -1,350 +1,350 @@ -/* eslint-disable react/destructuring-assignment */ -import { Button, Icon, Slider, Switch } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Tooltip2 } from '@blueprintjs/popover2'; -import React from 'react'; -import { AnimatedCurve } from '../../bundles/curve/types'; -import WebGLCanvas from '../common/webgl_canvas'; - -type Props = { - animation: AnimatedCurve; -}; - -type State = { - /** Timestamp of the animation */ - animTimestamp: number; - - /** Boolean value indicating if the animation is playing */ - isPlaying: boolean; - - /** Previous value of `isPlaying` */ - wasPlaying: boolean; - - /** Boolean value indicating if auto play is selected */ - autoPlay: boolean; - - /** Curve Angle */ - curveAngle: number; -}; - -export default class Curve3DAnimationCanvas extends React.Component< - Props, - State -> { - private canvas: HTMLCanvasElement | null; - - /** - * The duration of one frame in milliseconds - */ - private readonly frameDuration: number; - - /** - * The duration of the entire animation - */ - private readonly animationDuration: number; - - /** - * Last timestamp since the previous `requestAnimationFrame` call - */ - private callbackTimestamp: number | null; - - constructor(props: Props | Readonly) { - super(props); - - this.state = { - animTimestamp: 0, - isPlaying: false, - wasPlaying: false, - autoPlay: true, - curveAngle: 0, - }; - - this.canvas = null; - this.frameDuration = 1000 / props.animation.fps; - this.animationDuration = Math.round(props.animation.duration * 1000); - this.callbackTimestamp = null; - } - - public componentDidMount() { - this.drawFrame(); - } - - /** - * Call this to actually draw a frame onto the canvas - */ - private drawFrame = () => { - if (this.canvas) { - const frame = this.props.animation.getFrame( - this.state.animTimestamp / 1000 - ); - frame.draw(this.canvas); - } - }; - - private reqFrame = () => requestAnimationFrame(this.animationCallback); - - /** - * Callback to use with `requestAnimationFrame` - */ - private animationCallback = (timeInMs: number) => { - if (!this.canvas || !this.state.isPlaying) return; - - if (!this.callbackTimestamp) { - this.callbackTimestamp = timeInMs; - this.drawFrame(); - this.reqFrame(); - return; - } - - const currentFrame = timeInMs - this.callbackTimestamp; - - if (currentFrame < this.frameDuration) { - // Not time to draw a new frame yet - this.reqFrame(); - return; - } - - this.callbackTimestamp = timeInMs; - if (this.state.animTimestamp >= this.animationDuration) { - // Animation has ended - if (this.state.autoPlay) { - // If autoplay is active, reset the animation - this.setState( - { - animTimestamp: 0, - }, - this.reqFrame - ); - } else { - // Otherwise, stop the animation - this.setState( - { - isPlaying: false, - }, - () => { - this.callbackTimestamp = null; - } - ); - } - } else { - // Animation hasn't ended, so just draw the next frame - this.drawFrame(); - this.setState( - (prev) => ({ - animTimestamp: prev.animTimestamp + currentFrame, - }), - this.reqFrame - ); - } - }; - - /** - * Play button click handler - */ - private onPlayButtonClick = () => { - if (this.state.isPlaying) { - this.setState( - { - isPlaying: false, - }, - () => { - this.callbackTimestamp = null; - } - ); - } else { - this.setState( - { - isPlaying: true, - }, - this.reqFrame - ); - } - }; - - /** - * Reset button click handler - */ - private onResetButtonClick = () => { - this.setState( - { - animTimestamp: 0, - }, - () => { - if (this.state.isPlaying) this.reqFrame(); - else this.drawFrame(); - } - ); - }; - - /** - * Slider value change handler - * @param newValue New value of the slider - */ - private onTimeSliderChange = (newValue: number) => { - this.callbackTimestamp = null; - this.setState( - (prev) => ({ - wasPlaying: prev.isPlaying, - isPlaying: false, - animTimestamp: newValue, - }), - this.drawFrame - ); - }; - - /** - * Handler triggered when the slider is clicked off - */ - private onTimeSliderRelease = () => { - this.setState( - (prev) => ({ - isPlaying: prev.wasPlaying, - }), - () => { - if (!this.state.isPlaying) { - this.callbackTimestamp = null; - } else { - this.reqFrame(); - } - } - ); - }; - - private onAngleSliderChange = (newAngle: number) => { - this.setState( - { - curveAngle: newAngle, - }, - () => { - this.props.animation.angle = newAngle; - if (this.state.isPlaying) this.reqFrame(); - else this.drawFrame(); - } - ); - }; - - /** - * Auto play switch handler - */ - private autoPlaySwitchChanged = () => { - this.setState((prev) => ({ - autoPlay: !prev.autoPlay, - })); - }; - - public render() { - const buttons = ( -
    -
    - - - -
    - - - -
    - ); - - const sliders = ( -
    - - -
    - -
    -
    -
    - ); - - return ( - <> -
    - { - this.canvas = r; - }} - /> -
    -
    - {buttons} - {sliders} - -
    - - ); - } -} +/* eslint-disable react/destructuring-assignment */ +import { Button, Icon, Slider, Switch } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Tooltip2 } from '@blueprintjs/popover2'; +import React from 'react'; +import { AnimatedCurve } from '../../bundles/curve/types'; +import WebGLCanvas from '../common/webgl_canvas'; + +type Props = { + animation: AnimatedCurve; +}; + +type State = { + /** Timestamp of the animation */ + animTimestamp: number; + + /** Boolean value indicating if the animation is playing */ + isPlaying: boolean; + + /** Previous value of `isPlaying` */ + wasPlaying: boolean; + + /** Boolean value indicating if auto play is selected */ + autoPlay: boolean; + + /** Curve Angle */ + curveAngle: number; +}; + +export default class Curve3DAnimationCanvas extends React.Component< +Props, +State +> { + private canvas: HTMLCanvasElement | null; + + /** + * The duration of one frame in milliseconds + */ + private readonly frameDuration: number; + + /** + * The duration of the entire animation + */ + private readonly animationDuration: number; + + /** + * Last timestamp since the previous `requestAnimationFrame` call + */ + private callbackTimestamp: number | null; + + constructor(props: Props | Readonly) { + super(props); + + this.state = { + animTimestamp: 0, + isPlaying: false, + wasPlaying: false, + autoPlay: true, + curveAngle: 0, + }; + + this.canvas = null; + this.frameDuration = 1000 / props.animation.fps; + this.animationDuration = Math.round(props.animation.duration * 1000); + this.callbackTimestamp = null; + } + + public componentDidMount() { + this.drawFrame(); + } + + /** + * Call this to actually draw a frame onto the canvas + */ + private drawFrame = () => { + if (this.canvas) { + const frame = this.props.animation.getFrame( + this.state.animTimestamp / 1000, + ); + frame.draw(this.canvas); + } + }; + + private reqFrame = () => requestAnimationFrame(this.animationCallback); + + /** + * Callback to use with `requestAnimationFrame` + */ + private animationCallback = (timeInMs: number) => { + if (!this.canvas || !this.state.isPlaying) return; + + if (!this.callbackTimestamp) { + this.callbackTimestamp = timeInMs; + this.drawFrame(); + this.reqFrame(); + return; + } + + const currentFrame = timeInMs - this.callbackTimestamp; + + if (currentFrame < this.frameDuration) { + // Not time to draw a new frame yet + this.reqFrame(); + return; + } + + this.callbackTimestamp = timeInMs; + if (this.state.animTimestamp >= this.animationDuration) { + // Animation has ended + if (this.state.autoPlay) { + // If autoplay is active, reset the animation + this.setState( + { + animTimestamp: 0, + }, + this.reqFrame, + ); + } else { + // Otherwise, stop the animation + this.setState( + { + isPlaying: false, + }, + () => { + this.callbackTimestamp = null; + }, + ); + } + } else { + // Animation hasn't ended, so just draw the next frame + this.drawFrame(); + this.setState( + (prev) => ({ + animTimestamp: prev.animTimestamp + currentFrame, + }), + this.reqFrame, + ); + } + }; + + /** + * Play button click handler + */ + private onPlayButtonClick = () => { + if (this.state.isPlaying) { + this.setState( + { + isPlaying: false, + }, + () => { + this.callbackTimestamp = null; + }, + ); + } else { + this.setState( + { + isPlaying: true, + }, + this.reqFrame, + ); + } + }; + + /** + * Reset button click handler + */ + private onResetButtonClick = () => { + this.setState( + { + animTimestamp: 0, + }, + () => { + if (this.state.isPlaying) this.reqFrame(); + else this.drawFrame(); + }, + ); + }; + + /** + * Slider value change handler + * @param newValue New value of the slider + */ + private onTimeSliderChange = (newValue: number) => { + this.callbackTimestamp = null; + this.setState( + (prev) => ({ + wasPlaying: prev.isPlaying, + isPlaying: false, + animTimestamp: newValue, + }), + this.drawFrame, + ); + }; + + /** + * Handler triggered when the slider is clicked off + */ + private onTimeSliderRelease = () => { + this.setState( + (prev) => ({ + isPlaying: prev.wasPlaying, + }), + () => { + if (!this.state.isPlaying) { + this.callbackTimestamp = null; + } else { + this.reqFrame(); + } + }, + ); + }; + + private onAngleSliderChange = (newAngle: number) => { + this.setState( + { + curveAngle: newAngle, + }, + () => { + this.props.animation.angle = newAngle; + if (this.state.isPlaying) this.reqFrame(); + else this.drawFrame(); + }, + ); + }; + + /** + * Auto play switch handler + */ + private autoPlaySwitchChanged = () => { + this.setState((prev) => ({ + autoPlay: !prev.autoPlay, + })); + }; + + public render() { + const buttons = ( +
    +
    + + + +
    + + + +
    + ); + + const sliders = ( +
    + + +
    + +
    +
    +
    + ); + + return ( + <> +
    + { + this.canvas = r; + }} + /> +
    +
    + {buttons} + {sliders} + +
    + + ); + } +} diff --git a/src/tabs/Curve/curve_canvas3d.tsx b/src/tabs/Curve/curve_canvas3d.tsx index 33ac97a5c..32d873cbe 100644 --- a/src/tabs/Curve/curve_canvas3d.tsx +++ b/src/tabs/Curve/curve_canvas3d.tsx @@ -1,184 +1,184 @@ -/* eslint-disable react/destructuring-assignment */ -import { Slider, Button, Icon } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import React from 'react'; -import { CurveDrawn } from '../../bundles/curve/curves_webgl'; -import WebGLCanvas from '../common/webgl_canvas'; - -type State = { - /** - * Slider component reflects this value. This value is also passed in as - * argument to render curves. - */ - rotationAngle: number; - - /** - * Set to true by default. Slider updates this value to false when interacted - * with. Recursive `autoRotate()` checks for this value to decide whether to - * stop recursion. Button checks for this value to decide whether clicking the - * button takes effect, for countering spam-clicking. - */ - isRotating: boolean; - - displayAngle: boolean; -}; - -type Props = { - curve: CurveDrawn; -}; - -/** - * 3D Version of the CurveCanvas to include the rotation angle slider - * and play button - */ -export default class CurveCanvas3D extends React.Component { - private $canvas: HTMLCanvasElement | null; - - constructor(props) { - super(props); - - this.$canvas = null; - this.state = { - rotationAngle: 0, - isRotating: false, - displayAngle: false, - }; - } - - public componentDidMount() { - if (this.$canvas) { - this.props.curve.init(this.$canvas); - this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); - } - } - - /** - * Event handler for slider component. Updates the canvas for any change in - * rotation. - * - * @param newValue new rotation angle - */ - private onSliderChangeHandler = (newValue: number) => { - this.setState( - { - rotationAngle: newValue, - isRotating: false, - displayAngle: true, - }, - () => { - if (this.$canvas) { - this.props.curve.redraw((newValue / 180) * Math.PI); - } - } - ); - }; - - /** - * Event handler for play button. Starts automated rotation by calling - * `autoRotate()`. - */ - private onClickHandler = () => { - if (!this.$canvas) return; - - this.setState( - (prevState) => ({ - isRotating: !prevState.isRotating, - }), - () => { - if (this.state.isRotating) { - this.autoRotate(); - } - } - ); - }; - - /** - * Environment where `requestAnimationFrame` is called. - */ - private autoRotate = () => { - if (this.$canvas && this.state.isRotating) { - this.setState( - (prevState) => ({ - ...prevState, - rotationAngle: - prevState.rotationAngle >= 360 ? 0 : prevState.rotationAngle + 2, - }), - () => { - this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); - window.requestAnimationFrame(this.autoRotate); - } - ); - } - }; - - private onTextBoxChange = (event) => { - const angle = parseFloat(event.target.value); - this.setState( - () => ({ rotationAngle: angle }), - () => { - if (this.$canvas) { - this.props.curve.redraw((angle / 180) * Math.PI); - } - } - ); - }; - - public render() { - return ( -
    - { - this.$canvas = r; - }} - /> -
    - - - -
    -
    - ); - } -} +/* eslint-disable react/destructuring-assignment */ +import { Slider, Button, Icon } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import React from 'react'; +import { CurveDrawn } from '../../bundles/curve/curves_webgl'; +import WebGLCanvas from '../common/webgl_canvas'; + +type State = { + /** + * Slider component reflects this value. This value is also passed in as + * argument to render curves. + */ + rotationAngle: number; + + /** + * Set to true by default. Slider updates this value to false when interacted + * with. Recursive `autoRotate()` checks for this value to decide whether to + * stop recursion. Button checks for this value to decide whether clicking the + * button takes effect, for countering spam-clicking. + */ + isRotating: boolean; + + displayAngle: boolean; +}; + +type Props = { + curve: CurveDrawn; +}; + +/** + * 3D Version of the CurveCanvas to include the rotation angle slider + * and play button + */ +export default class CurveCanvas3D extends React.Component { + private $canvas: HTMLCanvasElement | null; + + constructor(props) { + super(props); + + this.$canvas = null; + this.state = { + rotationAngle: 0, + isRotating: false, + displayAngle: false, + }; + } + + public componentDidMount() { + if (this.$canvas) { + this.props.curve.init(this.$canvas); + this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); + } + } + + /** + * Event handler for slider component. Updates the canvas for any change in + * rotation. + * + * @param newValue new rotation angle + */ + private onSliderChangeHandler = (newValue: number) => { + this.setState( + { + rotationAngle: newValue, + isRotating: false, + displayAngle: true, + }, + () => { + if (this.$canvas) { + this.props.curve.redraw((newValue / 180) * Math.PI); + } + }, + ); + }; + + /** + * Event handler for play button. Starts automated rotation by calling + * `autoRotate()`. + */ + private onClickHandler = () => { + if (!this.$canvas) return; + + this.setState( + (prevState) => ({ + isRotating: !prevState.isRotating, + }), + () => { + if (this.state.isRotating) { + this.autoRotate(); + } + }, + ); + }; + + /** + * Environment where `requestAnimationFrame` is called. + */ + private autoRotate = () => { + if (this.$canvas && this.state.isRotating) { + this.setState( + (prevState) => ({ + ...prevState, + rotationAngle: + prevState.rotationAngle >= 360 ? 0 : prevState.rotationAngle + 2, + }), + () => { + this.props.curve.redraw((this.state.rotationAngle / 180) * Math.PI); + window.requestAnimationFrame(this.autoRotate); + }, + ); + } + }; + + private onTextBoxChange = (event) => { + const angle = parseFloat(event.target.value); + this.setState( + () => ({ rotationAngle: angle }), + () => { + if (this.$canvas) { + this.props.curve.redraw((angle / 180) * Math.PI); + } + }, + ); + }; + + public render() { + return ( +
    + { + this.$canvas = r; + }} + /> +
    + + + +
    +
    + ); + } +} diff --git a/src/tabs/Curve/index.tsx b/src/tabs/Curve/index.tsx index 4ab0bd569..593720b66 100644 --- a/src/tabs/Curve/index.tsx +++ b/src/tabs/Curve/index.tsx @@ -1,10 +1,10 @@ /* eslint-disable react/destructuring-assignment */ import React from 'react'; -import { CurveDrawn } from '../../bundles/curve/curves_webgl'; -import { AnimatedCurve, CurveModuleState } from '../../bundles/curve/types'; +import type { CurveDrawn } from '../../bundles/curve/curves_webgl'; +import type { AnimatedCurve, CurveModuleState } from '../../bundles/curve/types'; import { glAnimation } from '../../typings/anim_types'; import MultiItemDisplay from '../common/multi_item_display'; -import { DebuggerContext } from '../../typings/type_helpers'; +import type { DebuggerContext } from '../../typings/type_helpers'; import Curve3DAnimationCanvas from './3Dcurve_anim_canvas'; import CurveCanvas3D from './curve_canvas3d'; import AnimationCanvas from '../common/animation_canvas'; @@ -33,26 +33,30 @@ export default { if (glAnimation.isAnimation(curve)) { const anim = curve as AnimatedCurve; - return anim.is3D ? ( - - ) : ( - - ); + return anim.is3D + ? ( + + ) + : ( + + ); } const curveDrawn = curve as CurveDrawn; - return curveDrawn.is3D() ? ( - - ) : ( - { - if (r) { - curveDrawn.init(r); - curveDrawn.redraw(0); - } - }} - key={elemKey} - /> - ); + return curveDrawn.is3D() + ? ( + + ) + : ( + { + if (r) { + curveDrawn.init(r); + curveDrawn.redraw(0); + } + }} + key={elemKey} + /> + ); }); return ; diff --git a/src/tabs/Game/constants.ts b/src/tabs/Game/constants.ts index 3b29c71c2..c0775d539 100644 --- a/src/tabs/Game/constants.ts +++ b/src/tabs/Game/constants.ts @@ -1,6 +1,6 @@ -// eslint-disable-next-line import/prefer-default-export -export enum Links { - gameUserGuide = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-User-Guide', - gameDeveloperDocumentation = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-Developer-Documentation', - gameAPIDocumentation = 'https://source-academy.github.io/modules/documentation/modules/game.html', -} +// eslint-disable-next-line import/prefer-default-export +export enum Links { + gameUserGuide = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-User-Guide', + gameDeveloperDocumentation = 'https://github.com/source-academy/modules/wiki/%5Bgame%5D-Developer-Documentation', + gameAPIDocumentation = 'https://source-academy.github.io/modules/documentation/modules/game.html', +} diff --git a/src/tabs/Game/index.tsx b/src/tabs/Game/index.tsx index 47e5998c5..1e9132a04 100644 --- a/src/tabs/Game/index.tsx +++ b/src/tabs/Game/index.tsx @@ -1,41 +1,41 @@ -import React from 'react'; -import { Links } from './constants'; - -type Props = { - children?: never; - className?: string; - debuggerContext?: any; -}; - -class Game extends React.PureComponent { - public render() { - return ( -
    - Info: You need to visit the game to see the effect of your program. - Remember to save your work first! -
    -
    - You may find the game module{' '} - - documentation{' '} - - and{' '} - - user guide{' '} - - useful. -
    - ); - } -} - -export default { - toSpawn: () => true, - body: (debuggerContext: any) => , - label: 'Game Info Tab', - iconName: 'info-sign', -}; +import React from 'react'; +import { Links } from './constants'; + +type Props = { + children?: never; + className?: string; + debuggerContext?: any; +}; + +class Game extends React.PureComponent { + public render() { + return ( +
    + Info: You need to visit the game to see the effect of your program. + Remember to save your work first! +
    +
    + You may find the game module{' '} + + documentation{' '} + + and{' '} + + user guide{' '} + + useful. +
    + ); + } +} + +export default { + toSpawn: () => true, + body: (debuggerContext: any) => , + label: 'Game Info Tab', + iconName: 'info-sign', +}; diff --git a/src/tabs/MarkSweep/index.tsx b/src/tabs/MarkSweep/index.tsx index ef293a4af..c9af72ed5 100644 --- a/src/tabs/MarkSweep/index.tsx +++ b/src/tabs/MarkSweep/index.tsx @@ -57,9 +57,9 @@ class MarkSweep extends React.Component { componentDidMount() { const { debuggerContext } = this.props; if ( - debuggerContext && - debuggerContext.result && - debuggerContext.result.value + debuggerContext + && debuggerContext.result + && debuggerContext.result.value ) { this.initialize_state(); } @@ -178,8 +178,8 @@ class MarkSweep extends React.Component { private getlengthFunction = () => { const { debuggerContext } = this.props; - const commandHeap = - debuggerContext && debuggerContext.result.value + const commandHeap + = debuggerContext && debuggerContext.result.value ? debuggerContext.result.value.get_command() : []; return commandHeap.length; @@ -244,7 +244,7 @@ class MarkSweep extends React.Component { private renderLabel = (val: number) => { const { flips } = this.state; - return flips.includes(val) ? `^` : `${val}`; + return flips.includes(val) ? '^' : `${val}`; }; public render() { @@ -259,7 +259,7 @@ class MarkSweep extends React.Component {

    This is a visualiser for mark and sweep garbage collector. Check the guide{' '} - + here . @@ -267,7 +267,11 @@ class MarkSweep extends React.Component {

    {state.command}

    {state.description}

    {state.leftDesc && (
    @@ -281,30 +285,32 @@ class MarkSweep extends React.Component { {state.leftDesc}
    )} - {state.rightDesc ? ( -
    - - {state.rightDesc} -
    - ) : ( - false - )} + {state.rightDesc + ? ( +
    + + {state.rightDesc} +
    + ) + : ( + false + )}

    Current step: {' '} - + {' '} {state.value} {' '} - +

    {
    - {memoryMatrix && - memoryMatrix.length > 0 && - memoryMatrix.map((item, row) => ( -
    + {memoryMatrix + && memoryMatrix.length > 0 + && memoryMatrix.map((item, row) => ( +
    {row * state.column} - {item && - item.length > 0 && - item.map((content) => { + {item + && item.length > 0 + && item.map((content) => { const color = this.getMemoryColor(content); const bgColor = this.getBackgroundColor(content); return ( @@ -365,7 +374,11 @@ class MarkSweep extends React.Component {
    {
    MARK_SLOT: @@ -435,7 +452,7 @@ class MarkSweep extends React.Component {

    This is a visualiser for mark and sweep garbage collector. Check the guide{' '} - + here . diff --git a/src/tabs/MarkSweep/style.tsx b/src/tabs/MarkSweep/style.tsx index aa890ff50..454735823 100644 --- a/src/tabs/MarkSweep/style.tsx +++ b/src/tabs/MarkSweep/style.tsx @@ -1,13 +1,13 @@ -export enum ThemeColor { - BLUE = 'lightblue', - PINK = 'salmon', - GREY = '#707070', - GREEN = '#42a870', - YELLOW = '#f0d60e', - RED = 'red', - BLACK = 'black', -} - -export const FONT = { - SMALL: 10, -}; +export enum ThemeColor { + BLUE = 'lightblue', + PINK = 'salmon', + GREY = '#707070', + GREEN = '#42a870', + YELLOW = '#f0d60e', + RED = 'red', + BLACK = 'black', +} + +export const FONT = { + SMALL: 10, +}; diff --git a/src/tabs/Pixnflix/index.tsx b/src/tabs/Pixnflix/index.tsx index 2f81c58c6..fb35c9ef6 100644 --- a/src/tabs/Pixnflix/index.tsx +++ b/src/tabs/Pixnflix/index.tsx @@ -109,7 +109,7 @@ class PixNFlix extends React.Component { this.printError, { onClickStill: this.onClickStill, - } + }, ); let mode: VideoMode = VideoMode.Video; if (inputFeed === InputFeed.Local) { @@ -155,7 +155,7 @@ class PixNFlix extends React.Component { () => ({ mode: VideoMode.Still, }), - this.handleSnapPicture + this.handleSnapPicture, ); } }; @@ -167,7 +167,7 @@ class PixNFlix extends React.Component { () => ({ mode: VideoMode.Video, }), - this.handleStartVideo + this.handleStartVideo, ); } }; @@ -195,10 +195,10 @@ class PixNFlix extends React.Component { public handleUpdateDimensions = (w: number, h: number) => { if ( - w >= MIN_WIDTH && - w <= MAX_WIDTH && - h >= MIN_HEIGHT && - h <= MAX_HEIGHT + w >= MIN_WIDTH + && w <= MAX_WIDTH + && h >= MIN_HEIGHT + && h <= MAX_HEIGHT ) { this.setState({ width: w, @@ -264,9 +264,9 @@ class PixNFlix extends React.Component { */ private isPixNFlix() { return ( - this.pixNFlix && - this.pixNFlix.toReplString && - this.pixNFlix.toReplString() === '[Pix N Flix]' + this.pixNFlix + && this.pixNFlix.toReplString + && this.pixNFlix.toReplString() === '[Pix N Flix]' ); } @@ -277,38 +277,38 @@ class PixNFlix extends React.Component { const isAccepting = mode === VideoMode.Accepting; return (

    -
    +
    -
    +
    {/* */} { /> {/* */}
    -
    +
    {/* */} { /> {/* */}
    -
    +
    {/* */} {
    -
    +
    {/* eslint-disable-next-line jsx-a11y/alt-text */} { @@ -389,7 +389,7 @@ class PixNFlix extends React.Component {
    Drag file here

    - +

    { > Volume: void>(); - const animId = React.useRef(null); - - const animCallback = (timeInMs: number) => { - renderFuncRef.current!(timeInMs); - animId.current = requestAnimationFrame(animCallback); - }; - - React.useEffect(() => { - if (canvasRef.current) { - renderFuncRef.current = rune.draw(canvasRef.current!); - animCallback(0); - - return () => { - if (animId.current) { - cancelAnimationFrame(animId.current!); - } - }; - } - - return undefined; - }, []); - - return ; -} +/* eslint-disable react/destructuring-assignment */ +import React from 'react'; +import type { HollusionRune } from '../../bundles/rune/functions'; +import WebGLCanvas from '../common/webgl_canvas'; + +/** + * Canvas used to display Hollusion runes + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function HollusionCanvas({ rune }: { rune: HollusionRune }) { + const canvasRef = React.useRef(null); + const renderFuncRef = React.useRef<(time: number) => void>(); + const animId = React.useRef(null); + + const animCallback = (timeInMs: number) => { + renderFuncRef.current!(timeInMs); + animId.current = requestAnimationFrame(animCallback); + }; + + React.useEffect(() => { + if (canvasRef.current) { + renderFuncRef.current = rune.draw(canvasRef.current!); + animCallback(0); + + return () => { + if (animId.current) { + cancelAnimationFrame(animId.current!); + } + }; + } + + return undefined; + }, []); + + return ; +} diff --git a/src/tabs/Sound/index.tsx b/src/tabs/Sound/index.tsx index bf485d6f2..5499bc0fa 100644 --- a/src/tabs/Sound/index.tsx +++ b/src/tabs/Sound/index.tsx @@ -1,75 +1,75 @@ -/* eslint-disable react/destructuring-assignment */ -/* eslint-disable @typescript-eslint/no-unused-vars, jsx-a11y/media-has-caption */ -import React from 'react'; -import { SoundsModuleState } from '../../bundles/sound/types'; -import { DebuggerContext } from '../../typings/type_helpers'; -import MultiItemDisplay from '../common/multi_item_display'; - -/** - * Tab for Source Academy Sounds Module - * @author Koh Shang Hui - * @author Samyukta Sounderraman - */ - -export default { - /** - * This function will be called to determine if the component will be - * rendered. - * @returns {boolean} - */ - toSpawn(context: DebuggerContext) { - const moduleContext = context.context?.moduleContexts.get('sound'); - if (!moduleContext) { - return false; - } - - const moduleState = moduleContext.state as SoundsModuleState; - if (!moduleState) { - return false; - } - - return moduleState.audioPlayed.length > 0; - }, - /** - * This function will be called to render the module tab in the side contents - * on Source Academy frontend. - * @param {DebuggerContext} context - */ - body(context: DebuggerContext) { - const moduleContext = context.context?.moduleContexts.get('sound'); - const moduleState = (moduleContext!.state as SoundsModuleState).audioPlayed; - const elements = moduleState.map((audio) => ( -
    -
    +
    { this.$image = r; diff --git a/src/tabs/Rune/hollusion_canvas.tsx b/src/tabs/Rune/hollusion_canvas.tsx index c8409161b..b8c5d39ff 100644 --- a/src/tabs/Rune/hollusion_canvas.tsx +++ b/src/tabs/Rune/hollusion_canvas.tsx @@ -1,34 +1,34 @@ -import React from 'react'; -import { HollusionRune } from '../../bundles/rune/functions'; -import WebGLCanvas from '../common/webgl_canvas'; - -/** - * Canvas used to display Hollusion runes - */ -export default function HollusionCanvas({ rune }: { rune: HollusionRune }) { - const canvasRef = React.useRef(null); - const renderFuncRef = React.useRef<(time: number) => void>(); - const animId = React.useRef(null); - - const animCallback = (timeInMs: number) => { - renderFuncRef.current!(timeInMs); - animId.current = requestAnimationFrame(animCallback); - }; - - React.useEffect(() => { - if (canvasRef.current) { - renderFuncRef.current = rune.draw(canvasRef.current!); - animCallback(0); - - return () => { - if (animId.current) { - cancelAnimationFrame(animId.current!); - } - }; - } - - return undefined; - }, []); - - return ; -} +import React from 'react'; +import { HollusionRune } from '../../bundles/rune/functions'; +import WebGLCanvas from '../common/webgl_canvas'; + +/** + * Canvas used to display Hollusion runes + */ +export default function HollusionCanvas({ rune }: { rune: HollusionRune }) { + const canvasRef = React.useRef(null); + const renderFuncRef = React.useRef<(time: number) => void>(); + const animId = React.useRef(null); + + const animCallback = (timeInMs: number) => { + renderFuncRef.current!(timeInMs); + animId.current = requestAnimationFrame(animCallback); + }; + + React.useEffect(() => { + if (canvasRef.current) { + renderFuncRef.current = rune.draw(canvasRef.current!); + animCallback(0); + + return () => { + if (animId.current) { + cancelAnimationFrame(animId.current!); + } + }; + } + + return undefined; + }, []); + + return ; +} diff --git a/src/tabs/Sound/index.tsx b/src/tabs/Sound/index.tsx index b8e9758f5..69ba2d770 100644 --- a/src/tabs/Sound/index.tsx +++ b/src/tabs/Sound/index.tsx @@ -1,73 +1,73 @@ -import React from 'react'; -import { SoundsModuleState } from '../../bundles/sound/types'; -import { DebuggerContext } from '../../typings/type_helpers'; -import MultiItemDisplay from '../common/multi_item_display'; - -/** - * Tab for Source Academy Sounds Module - * @author Koh Shang Hui - * @author Samyukta Sounderraman - */ - -export default { - /** - * This function will be called to determine if the component will be - * rendered. - * @returns {boolean} - */ - toSpawn(context: DebuggerContext) { - const moduleContext = context.context?.moduleContexts.get('sound'); - if (!moduleContext) { - return false; - } - - const moduleState = moduleContext.state as SoundsModuleState; - if (!moduleState) { - return false; - } - - return moduleState.audioPlayed.length > 0; - }, - /** - * This function will be called to render the module tab in the side contents - * on Source Academy frontend. - * @param {DebuggerContext} context - */ - body(context: DebuggerContext) { - const moduleContext = context.context?.moduleContexts.get('sound'); - const moduleState = (moduleContext!.state as SoundsModuleState).audioPlayed; - const elements = moduleState.map((audio) => ( -