Finish enabling ts-eslint (#5944)

- prefer `null`, `!= null` and `== null` instead of `undefined`
- disallow `as`, add comments for the existing usages of `as`
- make `tsconfig.json` a bit stricter
- minor fixes to other files that were missed

# Important Notes
N/A
This commit is contained in:
somebody1234 2023-03-20 19:35:16 +10:00 committed by GitHub
parent 130fd803f0
commit fa23e800e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 275 additions and 250 deletions

View File

@ -1,4 +1,4 @@
/** Helper module for running esbuild in watch mode. */ /** @file A helper module for running esbuild in watch mode. */
import * as esbuild from 'esbuild' import * as esbuild from 'esbuild'
@ -6,21 +6,20 @@ import * as esbuild from 'esbuild'
* @param config - Configuration for the esbuild command. * @param config - Configuration for the esbuild command.
* @param onRebuild - Callback to be called after each rebuild. * @param onRebuild - Callback to be called after each rebuild.
* @param inject - See [esbuild docs](https://esbuild.github.io/api/#inject). * @param inject - See [esbuild docs](https://esbuild.github.io/api/#inject).
* */
**/ export function toWatchOptions<T extends esbuild.BuildOptions>(
export function toWatchOptions( config: T,
config: esbuild.BuildOptions,
onRebuild?: () => void, onRebuild?: () => void,
inject?: esbuild.BuildOptions['inject'] inject?: esbuild.BuildOptions['inject']
): esbuild.BuildOptions { ) {
return { return {
...config, ...config,
inject: [...(config.inject ?? []), ...(inject ?? [])], inject: [...(config.inject ?? []), ...(inject ?? [])],
watch: { watch: {
onRebuild(error, result) { onRebuild(error) {
if (error) console.error('watch build failed:', error) if (error) console.error('watch build failed:', error)
else onRebuild?.() else onRebuild?.()
}, },
}, },
} } satisfies esbuild.BuildOptions
} }

View File

@ -29,7 +29,7 @@ const ALLOWED_DEFAULT_IMPORT_MODULES = `${DEFAULT_IMPORT_ONLY_MODULES}|react-hot
const OUR_MODULES = 'enso-content-config|enso-common' const OUR_MODULES = 'enso-content-config|enso-common'
const RELATIVE_MODULES = const RELATIVE_MODULES =
'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|index|ipc|naming|paths|preload|security' 'bin\\u002Fproject-manager|bin\\u002Fserver|config\\u002Fparser|authentication|config|debug|index|ipc|naming|paths|preload|security'
const STRING_LITERAL = 'Literal[raw=/^["\']/]' const STRING_LITERAL = ':matches(Literal[raw=/^["\']/], TemplateLiteral)'
const JSX = ':matches(JSXElement, JSXFragment)' const JSX = ':matches(JSXElement, JSXFragment)'
const NOT_PASCAL_CASE = '/^(?!_?([A-Z][a-z0-9]*)+$)/' const NOT_PASCAL_CASE = '/^(?!_?([A-Z][a-z0-9]*)+$)/'
const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)/' const NOT_CAMEL_CASE = '/^(?!_?[a-z][a-z0-9*]*([A-Z0-9][a-z0-9]*)*$)/'
@ -102,7 +102,7 @@ const RESTRICTED_SYNTAXES = [
}, },
{ {
selector: selector:
':not(:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, SwitchStatement, SwitchCase, IfStatement:has(.consequent > :matches(ReturnStatement, ThrowStatement)):has(.alternate :matches(ReturnStatement, ThrowStatement)))) > * > ReturnStatement', ':not(:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, SwitchStatement, SwitchCase, IfStatement:has(.consequent > :matches(ReturnStatement, ThrowStatement)):has(.alternate :matches(ReturnStatement, ThrowStatement)), TryStatement:has(.block > :matches(ReturnStatement, ThrowStatement)):has(:matches([handler=null], .handler :matches(ReturnStatement, ThrowStatement))):has(:matches([finalizer=null], .finalizer :matches(ReturnStatement, ThrowStatement))))) > * > ReturnStatement',
message: 'No early returns', message: 'No early returns',
}, },
{ {
@ -139,8 +139,18 @@ const RESTRICTED_SYNTAXES = [
message: 'Use `as const` for top-level object literals only containing string literals', message: 'Use `as const` for top-level object literals only containing string literals',
}, },
{ {
selector: ':matches(TSNullKeyword, Literal[raw=null])', // Matches `as T` in either:
message: 'Use `undefined` instead of `null`', // - anything other than a variable declaration
// - a variable declaration that is not at the top level
// - a top-level variable declaration that shouldn't be `as const`
// - a top-level variable declaration that should be `as const`, but is `as SomeActualType` instead
selector: `:matches(:not(VariableDeclarator) > TSAsExpression, :not(:matches(Program, ExportNamedDeclaration)) > VariableDeclaration > * > TSAsExpression, :matches(Program, ExportNamedDeclaration) > VariableDeclaration > * > TSAsExpression > .expression:not(ObjectExpression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL})))), :matches(Program, ExportNamedDeclaration) > VariableDeclaration > * > TsAsExpression:not(:has(TSTypeReference > Identifier[name=const])) > ObjectExpression.expression:has(Property > ${STRING_LITERAL}.value):not(:has(Property > .value:not(${STRING_LITERAL}))))`,
message: 'Avoid `as T`. Consider using a type annotation instead.',
},
{
selector:
':matches(TSUndefinedKeyword, Identifier[name=undefined], UnaryExpression[operator=void]:not(:has(CallExpression.argument)), BinaryExpression[operator=/^===?$/]:has(UnaryExpression.left[operator=typeof]):has(Literal.right[value=undefined]))',
message: 'Use `null` instead of `undefined`, `void 0`, or `typeof x === "undefined"`',
}, },
{ {
selector: 'ExportNamedDeclaration > VariableDeclaration[kind=let]', selector: 'ExportNamedDeclaration > VariableDeclaration[kind=let]',
@ -216,7 +226,7 @@ export default [
...tsEslint.configs.recommended?.rules, ...tsEslint.configs.recommended?.rules,
...tsEslint.configs['recommended-requiring-type-checking']?.rules, ...tsEslint.configs['recommended-requiring-type-checking']?.rules,
...tsEslint.configs.strict?.rules, ...tsEslint.configs.strict?.rules,
eqeqeq: 'error', eqeqeq: ['error', 'always', { null: 'never' }],
'sort-imports': ['error', { allowSeparatedGroups: true }], 'sort-imports': ['error', { allowSeparatedGroups: true }],
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES], 'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
'prefer-arrow-callback': 'error', 'prefer-arrow-callback': 'error',

View File

@ -18,7 +18,6 @@ import yargs from 'yargs'
import * as common from 'enso-common' import * as common from 'enso-common'
import * as paths from './paths.js' import * as paths from './paths.js'
import * as shared from './shared.js'
import signArchivesMacOs from './tasks/signArchivesMacOs.js' import signArchivesMacOs from './tasks/signArchivesMacOs.js'
import BUILD_INFO from '../../build.json' assert { type: 'json' } import BUILD_INFO from '../../build.json' assert { type: 'json' }
@ -28,7 +27,9 @@ import BUILD_INFO from '../../build.json' assert { type: 'json' }
* @see `args` definition below for fields description. * @see `args` definition below for fields description.
*/ */
export interface Arguments { export interface Arguments {
target?: string // This is returned by a third-party library we do not control.
// eslint-disable-next-line no-restricted-syntax
target?: string | undefined
iconsDist: string iconsDist: string
guiDist: string guiDist: string
ideDist: string ideDist: string
@ -81,7 +82,7 @@ export const args: Arguments = await yargs(process.argv.slice(2))
export function createElectronBuilderConfig(passedArgs: Arguments): electronBuilder.Configuration { export function createElectronBuilderConfig(passedArgs: Arguments): electronBuilder.Configuration {
return { return {
appId: 'org.enso', appId: 'org.enso',
productName: shared.PRODUCT_NAME, productName: common.PRODUCT_NAME,
extraMetadata: { extraMetadata: {
version: BUILD_INFO.version, version: BUILD_INFO.version,
}, },
@ -104,13 +105,16 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
protocols: [ protocols: [
/** Electron URL protocol scheme definition for deep links to authentication flow pages. */ /** Electron URL protocol scheme definition for deep links to authentication flow pages. */
{ {
name: `${shared.PRODUCT_NAME} url`, name: `${common.PRODUCT_NAME} url`,
schemes: [common.DEEP_LINK_SCHEME], schemes: [common.DEEP_LINK_SCHEME],
role: 'Editor', role: 'Editor',
}, },
], ],
mac: { mac: {
// We do not use compression as the build time is huge and file size saving is almost zero. // We do not use compression as the build time is huge and file size saving is almost zero.
// This type assertion is UNSAFE, and any users MUST verify that
// they are passing a valid value to `target`.
// eslint-disable-next-line no-restricted-syntax
target: (passedArgs.target ?? 'dmg') as macOptions.MacOsTargetName, target: (passedArgs.target ?? 'dmg') as macOptions.MacOsTargetName,
icon: `${passedArgs.iconsDist}/icon.icns`, icon: `${passedArgs.iconsDist}/icon.icns`,
category: 'public.app-category.developer-tools', category: 'public.app-category.developer-tools',
@ -223,7 +227,9 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
console.log(' • Notarizing.') console.log(' • Notarizing.')
await electronNotarize.notarize({ await electronNotarize.notarize({
// This will always be defined since we set it at the top of this object. // This will always be defined since we set it at the top of this object.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // The type-cast is safe because this is only executes
// when `platform === electronBuilder.Platform.MAC`.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, no-restricted-syntax
appBundleId: (buildOptions as macOptions.MacConfiguration).appId!, appBundleId: (buildOptions as macOptions.MacConfiguration).appId!,
appPath: `${appOutDir}/${appName}.app`, appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID, appleId: process.env.APPLEID,

View File

@ -1,9 +0,0 @@
/** @file This module contains metadata about the product and distribution.
* For example, it contains:
* - the custom URL protocol scheme definitions.
*
* This metadata is used in both the code building the client resources and the packaged code
* itself. */
/** Name of the product. */
export const PRODUCT_NAME = 'Enso'

View File

@ -134,8 +134,8 @@ function initOpenUrlListener(window: () => electron.BrowserWindow) {
event.preventDefault() event.preventDefault()
if (parsedUrl.protocol !== `${common.DEEP_LINK_SCHEME}:`) { if (parsedUrl.protocol !== `${common.DEEP_LINK_SCHEME}:`) {
logger.error(`${url} is not a deep link, ignoring.`) logger.error(`${url} is not a deep link, ignoring.`)
return } else {
}
window().webContents.send(ipc.Channel.openDeepLink, url) window().webContents.send(ipc.Channel.openDeepLink, url)
}
}) })
} }

View File

@ -31,9 +31,7 @@ export function pathOrPanic(args: config.Args): string {
/** Executes the Project Manager with given arguments. */ /** Executes the Project Manager with given arguments. */
async function exec(args: config.Args, processArgs: string[]) { async function exec(args: config.Args, processArgs: string[]) {
const binPath = pathOrPanic(args) const binPath = pathOrPanic(args)
return await execFile(binPath, processArgs).catch(function (err) { return await execFile(binPath, processArgs)
throw err
})
} }
/** Spawn Project Manager process. /** Spawn Project Manager process.
@ -62,5 +60,7 @@ export function spawn(args: config.Args, processArgs: string[]): childProcess.Ch
export async function version(args: config.Args) { export async function version(args: config.Args) {
if (args.options.engine.value) { if (args.options.engine.value) {
return await exec(args, ['--version']).then(t => t.stdout) return await exec(args, ['--version']).then(t => t.stdout)
} else {
return
} }
} }

View File

@ -105,10 +105,9 @@ export class Server {
process(request: http.IncomingMessage, response: http.ServerResponse) { process(request: http.IncomingMessage, response: http.ServerResponse) {
const requestUrl = request.url const requestUrl = request.url
if (requestUrl === undefined) { if (requestUrl == null) {
logger.error('Request URL is null.') logger.error('Request URL is null.')
return } else {
}
const url = requestUrl.split('?')[0] const url = requestUrl.split('?')[0]
const resource = url === '/' ? '/index.html' : requestUrl const resource = url === '/' ? '/index.html' : requestUrl
const resourceFile = `${this.config.dir}${resource}` const resourceFile = `${this.config.dir}${resource}`
@ -127,4 +126,5 @@ export class Server {
} }
}) })
} }
}
} }

View File

@ -76,7 +76,7 @@ function printHelp(cfg: PrintHelpConfig) {
for (const [groupName, group] of Object.entries(cfg.args.groups)) { for (const [groupName, group] of Object.entries(cfg.args.groups)) {
let section = sections[groupName] let section = sections[groupName]
if (section === undefined) { if (section == null) {
section = new Section() section = new Section()
sections[groupName] = section sections[groupName] = section
} }
@ -95,7 +95,7 @@ function printHelp(cfg: PrintHelpConfig) {
const cmdOption = naming.camelToKebabCase(optionName) const cmdOption = naming.camelToKebabCase(optionName)
maxOptionLength = Math.max(maxOptionLength, stringLength(cmdOption)) maxOptionLength = Math.max(maxOptionLength, stringLength(cmdOption))
const section = sections[option.name] const section = sections[option.name]
if (section !== undefined) { if (section != null) {
section.entries.unshift([cmdOption, option]) section.entries.unshift([cmdOption, option])
} else { } else {
topLevelSection.entries.push([cmdOption, option]) topLevelSection.entries.push([cmdOption, option])
@ -127,12 +127,12 @@ function printHelp(cfg: PrintHelpConfig) {
? option.description ? option.description
: option.description.slice(0, firstSentenceSplit + 1) : option.description.slice(0, firstSentenceSplit + 1)
const otherSentences = option.description.slice(firstSentence.length) const otherSentences = option.description.slice(firstSentence.length)
const def = // We explicitly set the default for string options to be `null` in `parseArgs`.
option.defaultDescription ?? // eslint-disable-next-line no-restricted-syntax
((option.default ?? undefined) as string | undefined) const def = option.defaultDescription ?? (option.default as string | null)
const defIsEmptyArray = Array.isArray(def) && def.length === 0 const defIsEmptyArray = Array.isArray(def) && def.length === 0
let defaults = '' let defaults = ''
if (def !== undefined && def !== '' && !defIsEmptyArray) { if (def != null && def !== '' && !defIsEmptyArray) {
defaults = ` Defaults to ${chalk.green(def)}.` defaults = ` Defaults to ${chalk.green(def)}.`
} }
const description = firstSentence + defaults + chalk.gray(otherSentences) const description = firstSentence + defaults + chalk.gray(otherSentences)
@ -152,7 +152,7 @@ function wordWrap(str: string, width: number): string[] {
if (width <= 0) { if (width <= 0) {
logger.error(`Cannot perform word wrap. The output width is set to '${width}'.`) logger.error(`Cannot perform word wrap. The output width is set to '${width}'.`)
return [] return []
} } else {
let firstLine = true let firstLine = true
let line = '' let line = ''
const lines = [] const lines = []
@ -195,6 +195,7 @@ function wordWrap(str: string, width: number): string[] {
lines.push(line) lines.push(line)
} }
return lines return lines
}
} }
// ====================== // ======================
@ -205,7 +206,7 @@ export class ChromeOption {
constructor(public name: string, public value?: string) {} constructor(public name: string, public value?: string) {}
display(): string { display(): string {
const value = this.value === undefined ? '' : `=${this.value}` const value = this.value == null ? '' : `=${this.value}`
return `--${this.name}${value}` return `--${this.name}${value}`
} }
} }
@ -237,20 +238,20 @@ function argvAndChromeOptions(processArgs: string[]): ArgvAndChromeOptions {
const chromeOptions: ChromeOption[] = [] const chromeOptions: ChromeOption[] = []
for (let i = 0; i < processArgs.length; i++) { for (let i = 0; i < processArgs.length; i++) {
const processArg = processArgs[i] const processArg = processArgs[i]
if (processArg !== undefined) { if (processArg != null) {
const match = processArg.match(chromeOptionRegex) ?? undefined const match = processArg.match(chromeOptionRegex)
if (match?.[1] !== undefined) { if (match?.[1] != null) {
const optionName = match[1] const optionName = match[1]
const optionValue = match[2] const optionValue = match[2]
if (optionValue !== undefined) { if (optionValue != null) {
chromeOptions.push(new ChromeOption(optionName, optionValue)) chromeOptions.push(new ChromeOption(optionName, optionValue))
} else { } else {
const nextArgValue = processArgs[i + 1] const nextArgValue = processArgs[i + 1]
if (nextArgValue !== undefined && !nextArgValue.startsWith('-')) { if (nextArgValue != null && !nextArgValue.startsWith('-')) {
chromeOptions.push(new ChromeOption(optionName, nextArgValue)) chromeOptions.push(new ChromeOption(optionName, nextArgValue))
i++ i++
} else { } else {
chromeOptions.push(new ChromeOption(optionName, undefined)) chromeOptions.push(new ChromeOption(optionName))
} }
} }
} else { } else {
@ -274,19 +275,14 @@ export function parseArgs() {
const yargsOptions = args const yargsOptions = args
.optionsRecursive() .optionsRecursive()
.reduce((opts: Record<string, yargsModule.Options>, option) => { .reduce((opts: Record<string, yargsModule.Options>, option) => {
const yargsParam = Object.assign( opts[naming.camelToKebabCase(option.qualifiedName())] = {
{},
{
...option, ...option,
requiresArg: ['string', 'array'].includes(option.type), requiresArg: ['string', 'array'].includes(option.type),
default: undefined, default: null,
} // Required because yargs defines `defaultDescription`
) // as `string | undefined`, not `string | null`.
opts[naming.camelToKebabCase(option.qualifiedName())] = { // eslint-disable-next-line no-restricted-syntax
// Required because ensogl-pack has `defaultDescription` defaultDescription: option.defaultDescription ?? undefined,
// defined as `string | null` instead of `string | undefined` like in yargs
...yargsParam,
defaultDescription: yargsParam.defaultDescription ?? undefined,
} }
return opts return opts
}, {}) }, {})
@ -316,7 +312,7 @@ export function parseArgs() {
interface YargsArgs { interface YargsArgs {
// We don't control the naming of this third-party API. // We don't control the naming of this third-party API.
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
[key: string]: string[] | string | undefined [key: string]: string[] | string
_: string[] _: string[]
// Exists only when the `populate--` option is enabled. // Exists only when the `populate--` option is enabled.
'--'?: string[] '--'?: string[]
@ -324,27 +320,29 @@ export function parseArgs() {
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
} }
let parseError: Error | undefined // Required otherwise TypeScript thinks it's always `null`.
// eslint-disable-next-line no-restricted-syntax
let parseError = null as Error | null
// The type assertion is required since `parse` may return a `Promise`
// when an async middleware has been registered, but we are not doing that.
// eslint-disable-next-line no-restricted-syntax
const { '--': unexpectedArgs, ...parsedArgs } = optParser.parse( const { '--': unexpectedArgs, ...parsedArgs } = optParser.parse(
argv, argv,
{}, {},
// @ts-expect-error Yargs' typings are wrong. // @ts-expect-error Yargs' typings are wrong.
// eslint-disable-next-line no-restricted-syntax
(err: Error | null) => { (err: Error | null) => {
if (err) { if (err != null) {
parseError = err parseError = err
} }
} }
) as YargsArgs ) as YargsArgs
// The type assertion above is required since `parse` is defined to potentially return a `Promise`.
// This only happens when an async middleware has been registered though.
for (const option of args.optionsRecursive()) { for (const option of args.optionsRecursive()) {
const arg = parsedArgs[naming.camelToKebabCase(option.qualifiedName())] const arg = parsedArgs[naming.camelToKebabCase(option.qualifiedName())]
const isArray = Array.isArray(arg) const isArray = Array.isArray(arg)
// Yargs parses missing array options as `[undefined]`. // Yargs parses missing array options as `[undefined]`.
const isInvalidArray = isArray && arg.length === 1 && arg[0] === undefined const isInvalidArray = isArray && arg.length === 1 && arg[0] == null
if (arg !== undefined && !isInvalidArray) { if (arg != null && !isInvalidArray) {
option.value = arg option.value = arg
option.setByUser = true option.setByUser = true
} }
@ -386,10 +384,10 @@ export function parseArgs() {
const helpRequested = args.options.help.value || args.options.helpExtended.value const helpRequested = args.options.help.value || args.options.helpExtended.value
if (helpRequested) { if (helpRequested) {
printHelpAndExit() printHelpAndExit()
} else if (parseError !== undefined) { } else if (parseError != null) {
logger.error(parseError.message) logger.error(parseError.message)
printHelpAndExit(1) printHelpAndExit(1)
} else if (unexpectedArgs !== undefined) { } else if (unexpectedArgs != null) {
const unexpectedArgsString = unexpectedArgs.map(arg => JSON.stringify(arg)).join(' ') const unexpectedArgsString = unexpectedArgs.map(arg => JSON.stringify(arg)).join(' ')
logger.error(`Unexpected arguments found: '${unexpectedArgsString}'.`) logger.error(`Unexpected arguments found: '${unexpectedArgsString}'.`)
printHelpAndExit(1) printHelpAndExit(1)

View File

@ -50,5 +50,7 @@ async function getInfo() {
/** Print the current system information. */ /** Print the current system information. */
export async function printInfo() { export async function printInfo() {
const info = await getInfo() const info = await getInfo()
// This function does not accept `null` as its second parameter.
// eslint-disable-next-line no-restricted-syntax
console.log(JSON.stringify(info, undefined, INDENT_SIZE)) console.log(JSON.stringify(info, undefined, INDENT_SIZE))
} }

View File

@ -41,8 +41,8 @@ const INDENT_SIZE = 4
/** The Electron application. It is responsible for starting all the required services, and /** The Electron application. It is responsible for starting all the required services, and
* displaying and managing the app window. */ * displaying and managing the app window. */
class App { class App {
window: electron.BrowserWindow | undefined = undefined window: electron.BrowserWindow | null = null
server: server.Server | undefined = undefined server: server.Server | null = null
args: config.Args = config.CONFIG args: config.Args = config.CONFIG
isQuitting = false isQuitting = false
@ -194,7 +194,7 @@ class App {
frame: useFrame, frame: useFrame,
transparent: false, transparent: false,
titleBarStyle: useHiddenInsetTitleBar ? 'hiddenInset' : 'default', titleBarStyle: useHiddenInsetTitleBar ? 'hiddenInset' : 'default',
vibrancy: useVibrancy ? 'fullscreen-ui' : undefined, ...(useVibrancy ? { vibrancy: 'fullscreen-ui' } : {}),
} }
const window = new electron.BrowserWindow(windowPreferences) const window = new electron.BrowserWindow(windowPreferences)
window.setMenuBarVisibility(false) window.setMenuBarVisibility(false)
@ -272,7 +272,7 @@ class App {
/** Redirect the web view to `localhost:<port>` to see the served website. */ /** Redirect the web view to `localhost:<port>` to see the served website. */
loadWindowContent() { loadWindowContent() {
if (this.window !== undefined) { if (this.window != null) {
const urlCfg: Record<string, string> = {} const urlCfg: Record<string, string> = {}
for (const option of this.args.optionsRecursive()) { for (const option of this.args.optionsRecursive()) {
if (option.value !== option.default && option.passToWebApplication) { if (option.value !== option.default && option.passToWebApplication) {

View File

@ -34,7 +34,7 @@ electron.contextBridge.exposeInMainWorld('enso_lifecycle', {
// Save and load profile data. // Save and load profile data.
let onProfiles: ((profiles: string[]) => void)[] = [] let onProfiles: ((profiles: string[]) => void)[] = []
let profilesLoaded: string[] | undefined let profilesLoaded: string[] | null
electron.ipcRenderer.on(ipc.Channel.profilesLoaded, (_event, profiles: string[]) => { electron.ipcRenderer.on(ipc.Channel.profilesLoaded, (_event, profiles: string[]) => {
for (const callback of onProfiles) { for (const callback of onProfiles) {
callback(profiles) callback(profiles)
@ -49,7 +49,7 @@ electron.contextBridge.exposeInMainWorld('enso_profiling_data', {
}, },
// Requests any loaded profiling logs. // Requests any loaded profiling logs.
loadProfiles: (callback: (profiles: string[]) => void) => { loadProfiles: (callback: (profiles: string[]) => void) => {
if (profilesLoaded === undefined) { if (profilesLoaded == null) {
electron.ipcRenderer.send('load-profiles') electron.ipcRenderer.send('load-profiles')
onProfiles.push(callback) onProfiles.push(callback)
} else { } else {

View File

@ -25,14 +25,14 @@ function getChecksum(path, type) {
return new Promise( return new Promise(
// This JSDoc annotation is required for correct types that are also type-safe. // This JSDoc annotation is required for correct types that are also type-safe.
/** @param {(value: string) => void} resolve - Fulfill the promise with the given value. */ /** @param {(value: string) => void} resolve - Fulfill the promise with the given value. */
function (resolve, reject) { (resolve, reject) => {
const hash = cryptoModule.createHash(type) const hash = cryptoModule.createHash(type)
const input = fs.createReadStream(path) const input = fs.createReadStream(path)
input.on('error', reject) input.on('error', reject)
input.on('data', function (chunk) { input.on('data', chunk => {
hash.update(chunk) hash.update(chunk)
}) })
input.on('close', function () { input.on('close', () => {
resolve(hash.digest('hex')) resolve(hash.digest('hex'))
}) })
} }

View File

@ -9,3 +9,6 @@
* For example: the deep link URL * For example: the deep link URL
* `enso://authentication/register?code=...&state=...` uses this scheme. */ * `enso://authentication/register?code=...&state=...` uses this scheme. */
export const DEEP_LINK_SCHEME = 'enso' export const DEEP_LINK_SCHEME = 'enso'
/** Name of the product. */
export const PRODUCT_NAME = 'Enso'

View File

@ -106,12 +106,16 @@ export async function* filesToCopyProvider(wasmArtifacts: string, assetsPath: st
/** /**
* Generate the builder options. * Generate the builder options.
*/ */
export function bundlerOptions(args: Arguments): esbuild.BuildOptions { export function bundlerOptions(args: Arguments) {
const { outputPath, ensoglAppPath, wasmArtifacts, assetsPath } = args const { outputPath, ensoglAppPath, wasmArtifacts, assetsPath } = args
return { // This is required to make the `true` properties be typed as `boolean`.
// eslint-disable-next-line no-restricted-syntax
let trueBoolean = true as boolean
const buildOptions = {
// Disabling naming convention because these are third-party options.
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
absWorkingDir: THIS_PATH, absWorkingDir: THIS_PATH,
bundle: true, bundle: trueBoolean,
entryPoints: [path.resolve(THIS_PATH, 'src', 'index.ts')], entryPoints: [path.resolve(THIS_PATH, 'src', 'index.ts')],
outdir: outputPath, outdir: outputPath,
outbase: 'src', outbase: 'src',
@ -119,25 +123,23 @@ export function bundlerOptions(args: Arguments): esbuild.BuildOptions {
esbuildPluginYaml.yamlPlugin({}), esbuildPluginYaml.yamlPlugin({}),
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(), esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }), esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }),
// We do not control naming of third-party options.
esbuildPluginAlias({ ensogl_app: ensoglAppPath }), esbuildPluginAlias({ ensogl_app: ensoglAppPath }),
esbuildPluginTime(), esbuildPluginTime(),
esbuildPluginCopy.create(() => filesToCopyProvider(wasmArtifacts, assetsPath)), esbuildPluginCopy.create(() => filesToCopyProvider(wasmArtifacts, assetsPath)),
], ],
define: { define: {
// Disabling naming convention because these are third-party options.
GIT_HASH: JSON.stringify(git('rev-parse HEAD')), GIT_HASH: JSON.stringify(git('rev-parse HEAD')),
GIT_STATUS: JSON.stringify(git('status --short --porcelain')), GIT_STATUS: JSON.stringify(git('status --short --porcelain')),
BUILD_INFO: JSON.stringify(BUILD_INFO), BUILD_INFO: JSON.stringify(BUILD_INFO),
}, },
sourcemap: true, sourcemap: trueBoolean,
minify: true, minify: trueBoolean,
metafile: true, metafile: trueBoolean,
format: 'esm', format: 'esm',
publicPath: '/assets', publicPath: '/assets',
platform: 'browser', platform: 'browser',
incremental: true, incremental: trueBoolean,
color: true, color: trueBoolean,
logOverride: { logOverride: {
// Happens in ScalaJS-generated parser (scala-parser.js): // Happens in ScalaJS-generated parser (scala-parser.js):
// 6 │ "fileLevelThis": this // 6 │ "fileLevelThis": this
@ -153,14 +155,18 @@ export function bundlerOptions(args: Arguments): esbuild.BuildOptions {
'suspicious-boolean-not': 'silent', 'suspicious-boolean-not': 'silent',
}, },
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */
} } satisfies esbuild.BuildOptions
// The narrower type is required to avoid non-null assertions elsewhere.
// The intersection with `esbuild.BuildOptions` is required to allow mutation.
const correctlyTypedBuildOptions: esbuild.BuildOptions & typeof buildOptions = buildOptions
return correctlyTypedBuildOptions
} }
/** The basic, common settings for the bundler, based on the environment variables. /** The basic, common settings for the bundler, based on the environment variables.
* *
* Note that they should be further customized as per the needs of the specific workflow (e.g. watch vs. build). * Note that they should be further customized as per the needs of the specific workflow (e.g. watch vs. build).
*/ */
export function bundlerOptionsFromEnv(): esbuild.BuildOptions { export function bundlerOptionsFromEnv() {
return bundlerOptions(argumentsFromEnv()) return bundlerOptions(argumentsFromEnv())
} }

View File

@ -59,9 +59,7 @@ async function checkMinSupportedVersion(config: typeof contentConfig.OPTIONS) {
) )
if ( if (
typeof appConfig === 'object' && typeof appConfig === 'object' &&
// `typeof x === 'object'` narrows to `object | null`, not `object | undefined` appConfig != null &&
// eslint-disable-next-line no-restricted-syntax
appConfig !== null &&
'minimumSupportedVersion' in appConfig 'minimumSupportedVersion' in appConfig
) { ) {
const minSupportedVersion = appConfig.minimumSupportedVersion const minSupportedVersion = appConfig.minimumSupportedVersion
@ -88,13 +86,13 @@ function displayDeprecatedVersionDialog() {
'This version is no longer supported. Please download a new one.' 'This version is no longer supported. Please download a new one.'
) )
const root = document.getElementById('root') ?? undefined const root = document.getElementById('root')
const versionCheckDiv = document.createElement('div') const versionCheckDiv = document.createElement('div')
versionCheckDiv.id = 'version-check' versionCheckDiv.id = 'version-check'
versionCheckDiv.className = 'auth-info' versionCheckDiv.className = 'auth-info'
versionCheckDiv.style.display = 'block' versionCheckDiv.style.display = 'block'
versionCheckDiv.appendChild(versionCheckText) versionCheckDiv.appendChild(versionCheckText)
if (root === undefined) { if (root == null) {
console.error('Cannot find the root DOM element.') console.error('Cannot find the root DOM element.')
} else { } else {
root.appendChild(versionCheckDiv) root.appendChild(versionCheckDiv)

View File

@ -49,7 +49,7 @@ export class ProjectManager {
name: name, name: name,
missingComponentAction: action, missingComponentAction: action,
} }
if (template !== undefined) { if (template != null) {
Object.assign(params, { Object.assign(params, {
projectTemplate: template, projectTemplate: template,
}) })

View File

@ -9,4 +9,4 @@ const OPTS = bundler.bundleOptions()
const ROOT = OPTS.outdir const ROOT = OPTS.outdir
const ASSETS = ROOT const ASSETS = ROOT
await esbuild.build(OPTS) await esbuild.build(OPTS)
await guiServer.start({ root: ROOT, assets: ASSETS }) await guiServer.start({ root: ROOT, assets: ASSETS ?? null })

View File

@ -11,5 +11,5 @@ const OPTS = bundler.watchOptions(() => {
await esbuild.build(OPTS) await esbuild.build(OPTS)
const LIVE_SERVER = await guiServer.start({ const LIVE_SERVER = await guiServer.start({
root: OPTS.outdir, root: OPTS.outdir,
assets: OPTS.outdir, assets: OPTS.outdir ?? null,
}) })

View File

@ -79,10 +79,10 @@ export function create(filesProvider) {
} }
}) })
build.onLoad({ filter: /.*/, namespace: PLUGIN_NAME }, async () => { build.onLoad({ filter: /.*/, namespace: PLUGIN_NAME }, async () => {
if (build.initialOptions.outdir === undefined) { if (build.initialOptions.outdir == null) {
console.error('`copy-plugin` requires `outdir` to be specified.') console.error('`copy-plugin` requires `outdir` to be specified.')
return return
} } else {
let watchFiles = [] let watchFiles = []
for await (const file of files) { for await (const file of files) {
const to = path.join(build.initialOptions.outdir, path.basename(file)) const to = path.join(build.initialOptions.outdir, path.basename(file))
@ -94,6 +94,7 @@ export function create(filesProvider) {
contents: '', contents: '',
watchFiles, watchFiles,
} }
}
}) })
build.onEnd(() => { build.onEnd(() => {
// Replace with empty `AsyncGenerator`. // Replace with empty `AsyncGenerator`.

View File

@ -107,6 +107,7 @@ function intoAmplifyErrorOrThrow(error: unknown): AmplifyError {
* The caller can then handle them via pattern matching on the {@link results.Result} type. */ * The caller can then handle them via pattern matching on the {@link results.Result} type. */
export class Cognito { export class Cognito {
constructor( constructor(
// @ts-expect-error This will be used in a future PR.
private readonly logger: loggerProvider.Logger, private readonly logger: loggerProvider.Logger,
private readonly platform: platformModule.Platform, private readonly platform: platformModule.Platform,
amplifyConfig: config.AmplifyConfig amplifyConfig: config.AmplifyConfig

View File

@ -130,7 +130,7 @@ export function toNestedAmplifyConfig(config: AmplifyConfig): NestedAmplifyConfi
userPoolWebClientId: config.userPoolWebClientId, userPoolWebClientId: config.userPoolWebClientId,
oauth: { oauth: {
options: { options: {
urlOpener: config.urlOpener, ...(config.urlOpener ? { urlOpener: config.urlOpener } : {}),
}, },
domain: config.domain, domain: config.domain,
scope: config.scope, scope: config.scope,

View File

@ -31,7 +31,7 @@ export enum AuthEvent {
/** Returns `true` if the given `string` is an {@link AuthEvent}. */ /** Returns `true` if the given `string` is an {@link AuthEvent}. */
function isAuthEvent(value: string): value is AuthEvent { function isAuthEvent(value: string): value is AuthEvent {
return Object.values(AuthEvent).includes(value as AuthEvent); return Object.values<string>(AuthEvent).includes(value);
} }
// ================================= // =================================

View File

@ -81,8 +81,8 @@ interface AuthContextType {
signInWithPassword: (email: string, password: string) => Promise<void>; signInWithPassword: (email: string, password: string) => Promise<void>;
/** Session containing the currently authenticated user's authentication information. /** Session containing the currently authenticated user's authentication information.
* *
* If the user has not signed in, the session will be `undefined`. */ * If the user has not signed in, the session will be `null`. */
session: UserSession | undefined; session: UserSession | null;
} }
// Eslint doesn't like headings. // Eslint doesn't like headings.
@ -94,7 +94,7 @@ interface AuthContextType {
* An `as ...` cast is unsafe. We use this cast when creating the context. So it appears that the * An `as ...` cast is unsafe. We use this cast when creating the context. So it appears that the
* `AuthContextType` can be unsafely (i.e., only partially) initialized as a result of this. * `AuthContextType` can be unsafely (i.e., only partially) initialized as a result of this.
* *
* So it appears that we should remove the cast and initialize the context as `undefined` instead. * So it appears that we should remove the cast and initialize the context as `null` instead.
* *
* **However**, initializing a context the existing way is the recommended way to initialize a * **However**, initializing a context the existing way is the recommended way to initialize a
* context in React. It is safe, for non-obvious reasons. It is safe because the `AuthContext` is * context in React. It is safe, for non-obvious reasons. It is safe because the `AuthContext` is
@ -131,8 +131,8 @@ export function AuthProvider(props: AuthProviderProps) {
const navigate = router.useNavigate(); const navigate = router.useNavigate();
const onAuthenticated = react.useCallback(props.onAuthenticated, []); const onAuthenticated = react.useCallback(props.onAuthenticated, []);
const [initialized, setInitialized] = react.useState(false); const [initialized, setInitialized] = react.useState(false);
const [userSession, setUserSession] = react.useState<UserSession | undefined>( const [userSession, setUserSession] = react.useState<UserSession | null>(
undefined null
); );
/** Fetch the JWT access token from the session via the AWS Amplify library. /** Fetch the JWT access token from the session via the AWS Amplify library.
@ -144,7 +144,7 @@ export function AuthProvider(props: AuthProviderProps) {
const fetchSession = async () => { const fetchSession = async () => {
if (session.none) { if (session.none) {
setInitialized(true); setInitialized(true);
setUserSession(undefined); setUserSession(null);
} else { } else {
const { accessToken, email } = session.val; const { accessToken, email } = session.val;

View File

@ -148,7 +148,7 @@ function loadAmplifyConfig(
return { return {
...baseConfig, ...baseConfig,
...platformConfig, ...platformConfig,
urlOpener, ...(urlOpener ? { urlOpener } : {}),
}; };
} }
@ -225,13 +225,14 @@ function handleAuthResponse(url: string) {
try { try {
/** # Safety /** # Safety
* *
* It is safe to disable the `no-unsafe-member-access` and `no-unsafe-call` lints here * It is safe to disable the `no-unsafe-call` lint here
* because we know that the `Auth` object has the `_handleAuthResponse` method, and we * because we know that the `Auth` object has the `_handleAuthResponse` method, and we
* know that it is safe to call it with the `url` argument. There is no way to prove * know that it is safe to call it with the `url` argument. There is no way to prove
* this to the TypeScript compiler, because these methods are intentionally not part of * this to the TypeScript compiler, because these methods are intentionally not part of
* the public AWS Amplify API. */ * the public AWS Amplify API. */
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any // @ts-expect-error `_handleAuthResponse` is a private method without typings.
await (amplify.Auth as any)._handleAuthResponse(url); // eslint-disable-next-line @typescript-eslint/no-unsafe-call
await amplify.Auth._handleAuthResponse(url);
} finally { } finally {
/** Restore the original `window.location.replaceState` function. */ /** Restore the original `window.location.replaceState` function. */
window.history.replaceState = replaceState; window.history.replaceState = replaceState;

View File

@ -11,13 +11,11 @@
/** Path data for the SVG icons used in app. */ /** Path data for the SVG icons used in app. */
export const PATHS = { export const PATHS = {
/** Path data for the `@` icon SVG. */ /** Path data for the `@` icon SVG. */
at: at: `M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 \
"M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 " + 8.959 0 01-4.5 1.207`,
"8.959 0 01-4.5 1.207",
/** Path data for the lock icon SVG. */ /** Path data for the lock icon SVG. */
lock: lock: `M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 \
"M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 " + 0 00-8 0v4h8z`,
"0 00-8 0v4h8z",
/** Path data for the "right arrow" icon SVG. */ /** Path data for the "right arrow" icon SVG. */
rightArrow: "M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z", rightArrow: "M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z",
/** Path data for the "create account" icon SVG. */ /** Path data for the "create account" icon SVG. */

View File

@ -8,6 +8,7 @@
* included in the final bundle. */ * included in the final bundle. */
// It is safe to disable `no-restricted-syntax` because the `PascalCase` naming is required // It is safe to disable `no-restricted-syntax` because the `PascalCase` naming is required
// as per the above comment. // as per the above comment.
// @ts-expect-error See above comment for why this import is needed.
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-restricted-syntax // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-restricted-syntax
import * as React from "react"; import * as React from "react";
import * as reactDOM from "react-dom/client"; import * as reactDOM from "react-dom/client";

View File

@ -21,6 +21,8 @@ export interface Logger {
// === LoggerContext === // === LoggerContext ===
// ===================== // =====================
/** See {@link AuthContext} for safety details. */
// eslint-disable-next-line no-restricted-syntax
const LoggerContext = react.createContext<Logger>({} as Logger); const LoggerContext = react.createContext<Logger>({} as Logger);
// ====================== // ======================

View File

@ -146,8 +146,6 @@ async function genIcons(outputDir) {
return return
} else { } else {
console.log(`Generating icons to ${outputDir}`) console.log(`Generating icons to ${outputDir}`)
}
console.log('Generating SVG icons.') console.log('Generating SVG icons.')
await fs.mkdir(path.resolve(outputDir, 'svg'), { recursive: true }) await fs.mkdir(path.resolve(outputDir, 'svg'), { recursive: true })
await fs.mkdir(path.resolve(outputDir, 'png'), { recursive: true }) await fs.mkdir(path.resolve(outputDir, 'png'), { recursive: true })
@ -207,6 +205,8 @@ async function genIcons(outputDir) {
let handle = await fs.open(donePath, 'w') let handle = await fs.open(donePath, 'w')
await handle.close() await handle.close()
return
}
} }
/** Main entry function. */ /** Main entry function. */

View File

@ -22,7 +22,7 @@ export const LIVE_RELOAD_LISTENER_PATH = path.join(DIR_NAME, 'live-reload.js')
/** Start the server. /** Start the server.
* *
* @param {{ root: string; assets?: string; port?: number; }} options - Configuration options for this server. * @param {{ root: string; assets?: string | null; port?: number; }} options - Configuration options for this server.
*/ */
export async function start({ root, assets, port }) { export async function start({ root, assets, port }) {
assets = assets ?? path.join(root, 'assets') assets = assets ?? path.join(root, 'assets')

View File

@ -4,8 +4,9 @@ declare module 'enso-gui-server' {
export const LIVE_RELOAD_LISTENER_PATH: string export const LIVE_RELOAD_LISTENER_PATH: string
interface StartParams { interface StartParams {
root?: string // These are not values we explicitly supply
assets?: string root: string
assets?: string | null
port?: number port?: number
} }
interface ExectionInfo { interface ExectionInfo {

View File

@ -48,6 +48,8 @@ declare module 'create-servers' {
} }
export default function ( export default function (
option: CreateServersOptions, option: CreateServersOptions,
// This is a third-party module which we have no control over.
// eslint-disable-next-line no-restricted-syntax
errorHandler: (err: HttpError | undefined) => void errorHandler: (err: HttpError | undefined) => void
): unknown ): unknown
} }

View File

@ -7,16 +7,18 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"checkJs": true, "checkJs": true,
"noImplicitOverride": true, "strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true, "noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"removeComments": true, "removeComments": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "ES2019", "target": "ES2019",
"jsx": "react-jsx" "jsx": "react-jsx"
}, },

View File

@ -39,6 +39,9 @@ export function requireEnvResolvedPath(name: string) {
*/ */
export function requireEnvPathExist(name: string) { export function requireEnvPathExist(name: string) {
const value = requireEnv(name) const value = requireEnv(name)
if (fs.existsSync(value)) return value if (fs.existsSync(value)) {
else throw Error(`File with path ${value} read from environment variable ${name} is missing.`) return value
} else {
throw Error(`File with path ${value} read from environment variable ${name} is missing.`)
}
} }