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'
@ -6,21 +6,20 @@ import * as esbuild from 'esbuild'
* @param config - Configuration for the esbuild command.
* @param onRebuild - Callback to be called after each rebuild.
* @param inject - See [esbuild docs](https://esbuild.github.io/api/#inject).
*
**/
export function toWatchOptions(
config: esbuild.BuildOptions,
*/
export function toWatchOptions<T extends esbuild.BuildOptions>(
config: T,
onRebuild?: () => void,
inject?: esbuild.BuildOptions['inject']
): esbuild.BuildOptions {
) {
return {
...config,
inject: [...(config.inject ?? []), ...(inject ?? [])],
watch: {
onRebuild(error, result) {
onRebuild(error) {
if (error) console.error('watch build failed:', error)
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 RELATIVE_MODULES =
'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 NOT_PASCAL_CASE = '/^(?!_?([A-Z][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:
':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',
},
{
@ -139,8 +139,18 @@ const RESTRICTED_SYNTAXES = [
message: 'Use `as const` for top-level object literals only containing string literals',
},
{
selector: ':matches(TSNullKeyword, Literal[raw=null])',
message: 'Use `undefined` instead of `null`',
// Matches `as T` in either:
// - 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]',
@ -216,7 +226,7 @@ export default [
...tsEslint.configs.recommended?.rules,
...tsEslint.configs['recommended-requiring-type-checking']?.rules,
...tsEslint.configs.strict?.rules,
eqeqeq: 'error',
eqeqeq: ['error', 'always', { null: 'never' }],
'sort-imports': ['error', { allowSeparatedGroups: true }],
'no-restricted-syntax': ['error', ...RESTRICTED_SYNTAXES],
'prefer-arrow-callback': 'error',

View File

@ -18,7 +18,6 @@ import yargs from 'yargs'
import * as common from 'enso-common'
import * as paths from './paths.js'
import * as shared from './shared.js'
import signArchivesMacOs from './tasks/signArchivesMacOs.js'
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.
*/
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
guiDist: string
ideDist: string
@ -81,7 +82,7 @@ export const args: Arguments = await yargs(process.argv.slice(2))
export function createElectronBuilderConfig(passedArgs: Arguments): electronBuilder.Configuration {
return {
appId: 'org.enso',
productName: shared.PRODUCT_NAME,
productName: common.PRODUCT_NAME,
extraMetadata: {
version: BUILD_INFO.version,
},
@ -104,13 +105,16 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
protocols: [
/** 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],
role: 'Editor',
},
],
mac: {
// 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,
icon: `${passedArgs.iconsDist}/icon.icns`,
category: 'public.app-category.developer-tools',
@ -223,7 +227,9 @@ export function createElectronBuilderConfig(passedArgs: Arguments): electronBuil
console.log(' • Notarizing.')
await electronNotarize.notarize({
// 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!,
appPath: `${appOutDir}/${appName}.app`,
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()
if (parsedUrl.protocol !== `${common.DEEP_LINK_SCHEME}:`) {
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. */
async function exec(args: config.Args, processArgs: string[]) {
const binPath = pathOrPanic(args)
return await execFile(binPath, processArgs).catch(function (err) {
throw err
})
return await execFile(binPath, processArgs)
}
/** 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) {
if (args.options.engine.value) {
return await exec(args, ['--version']).then(t => t.stdout)
} else {
return
}
}

View File

@ -105,26 +105,26 @@ export class Server {
process(request: http.IncomingMessage, response: http.ServerResponse) {
const requestUrl = request.url
if (requestUrl === undefined) {
if (requestUrl == null) {
logger.error('Request URL is null.')
return
}
const url = requestUrl.split('?')[0]
const resource = url === '/' ? '/index.html' : requestUrl
const resourceFile = `${this.config.dir}${resource}`
fs.readFile(resourceFile, (err, data) => {
if (err) {
logger.error(`Resource '${resource}' not found.`)
} else {
const contentType = mime.contentType(path.extname(resourceFile))
const contentLength = data.length
if (contentType !== false) {
response.setHeader('Content-Type', contentType)
} else {
const url = requestUrl.split('?')[0]
const resource = url === '/' ? '/index.html' : requestUrl
const resourceFile = `${this.config.dir}${resource}`
fs.readFile(resourceFile, (err, data) => {
if (err) {
logger.error(`Resource '${resource}' not found.`)
} else {
const contentType = mime.contentType(path.extname(resourceFile))
const contentLength = data.length
if (contentType !== false) {
response.setHeader('Content-Type', contentType)
}
response.setHeader('Content-Length', contentLength)
response.writeHead(HTTP_STATUS_OK)
response.end(data)
}
response.setHeader('Content-Length', contentLength)
response.writeHead(HTTP_STATUS_OK)
response.end(data)
}
})
})
}
}
}

View File

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

View File

@ -50,5 +50,7 @@ async function getInfo() {
/** Print the current system information. */
export async function printInfo() {
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))
}

View File

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

View File

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

View File

@ -25,14 +25,14 @@ function getChecksum(path, type) {
return new Promise(
// 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. */
function (resolve, reject) {
(resolve, reject) => {
const hash = cryptoModule.createHash(type)
const input = fs.createReadStream(path)
input.on('error', reject)
input.on('data', function (chunk) {
input.on('data', chunk => {
hash.update(chunk)
})
input.on('close', function () {
input.on('close', () => {
resolve(hash.digest('hex'))
})
}

View File

@ -9,3 +9,6 @@
* For example: the deep link URL
* `enso://authentication/register?code=...&state=...` uses this scheme. */
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.
*/
export function bundlerOptions(args: Arguments): esbuild.BuildOptions {
export function bundlerOptions(args: Arguments) {
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 */
absWorkingDir: THIS_PATH,
bundle: true,
bundle: trueBoolean,
entryPoints: [path.resolve(THIS_PATH, 'src', 'index.ts')],
outdir: outputPath,
outbase: 'src',
@ -119,25 +123,23 @@ export function bundlerOptions(args: Arguments): esbuild.BuildOptions {
esbuildPluginYaml.yamlPlugin({}),
esbuildPluginNodeModules.NodeModulesPolyfillPlugin(),
esbuildPluginNodeGlobals.NodeGlobalsPolyfillPlugin({ buffer: true, process: true }),
// We do not control naming of third-party options.
esbuildPluginAlias({ ensogl_app: ensoglAppPath }),
esbuildPluginTime(),
esbuildPluginCopy.create(() => filesToCopyProvider(wasmArtifacts, assetsPath)),
],
define: {
// Disabling naming convention because these are third-party options.
GIT_HASH: JSON.stringify(git('rev-parse HEAD')),
GIT_STATUS: JSON.stringify(git('status --short --porcelain')),
BUILD_INFO: JSON.stringify(BUILD_INFO),
},
sourcemap: true,
minify: true,
metafile: true,
sourcemap: trueBoolean,
minify: trueBoolean,
metafile: trueBoolean,
format: 'esm',
publicPath: '/assets',
platform: 'browser',
incremental: true,
color: true,
incremental: trueBoolean,
color: trueBoolean,
logOverride: {
// Happens in ScalaJS-generated parser (scala-parser.js):
// 6 │ "fileLevelThis": this
@ -153,14 +155,18 @@ export function bundlerOptions(args: Arguments): esbuild.BuildOptions {
'suspicious-boolean-not': 'silent',
},
/* 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.
*
* 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())
}

View File

@ -59,9 +59,7 @@ async function checkMinSupportedVersion(config: typeof contentConfig.OPTIONS) {
)
if (
typeof appConfig === 'object' &&
// `typeof x === 'object'` narrows to `object | null`, not `object | undefined`
// eslint-disable-next-line no-restricted-syntax
appConfig !== null &&
appConfig != null &&
'minimumSupportedVersion' in appConfig
) {
const minSupportedVersion = appConfig.minimumSupportedVersion
@ -88,13 +86,13 @@ function displayDeprecatedVersionDialog() {
'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')
versionCheckDiv.id = 'version-check'
versionCheckDiv.className = 'auth-info'
versionCheckDiv.style.display = 'block'
versionCheckDiv.appendChild(versionCheckText)
if (root === undefined) {
if (root == null) {
console.error('Cannot find the root DOM element.')
} else {
root.appendChild(versionCheckDiv)

View File

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

View File

@ -9,4 +9,4 @@ const OPTS = bundler.bundleOptions()
const ROOT = OPTS.outdir
const ASSETS = ROOT
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)
const LIVE_SERVER = await guiServer.start({
root: OPTS.outdir,
assets: OPTS.outdir,
assets: OPTS.outdir ?? null,
})

View File

@ -79,20 +79,21 @@ export function create(filesProvider) {
}
})
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.')
return
}
let watchFiles = []
for await (const file of files) {
const to = path.join(build.initialOptions.outdir, path.basename(file))
await copy(file, to)
watchFiles.push(file)
}
console.log('Copied files.', watchFiles)
return {
contents: '',
watchFiles,
} else {
let watchFiles = []
for await (const file of files) {
const to = path.join(build.initialOptions.outdir, path.basename(file))
await copy(file, to)
watchFiles.push(file)
}
console.log('Copied files.', watchFiles)
return {
contents: '',
watchFiles,
}
}
})
build.onEnd(() => {

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. */
export class Cognito {
constructor(
// @ts-expect-error This will be used in a future PR.
private readonly logger: loggerProvider.Logger,
private readonly platform: platformModule.Platform,
amplifyConfig: config.AmplifyConfig

View File

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

View File

@ -31,7 +31,7 @@ export enum AuthEvent {
/** Returns `true` if the given `string` is an {@link 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>;
/** Session containing the currently authenticated user's authentication information.
*
* If the user has not signed in, the session will be `undefined`. */
session: UserSession | undefined;
* If the user has not signed in, the session will be `null`. */
session: UserSession | null;
}
// 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
* `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
* 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 onAuthenticated = react.useCallback(props.onAuthenticated, []);
const [initialized, setInitialized] = react.useState(false);
const [userSession, setUserSession] = react.useState<UserSession | undefined>(
undefined
const [userSession, setUserSession] = react.useState<UserSession | null>(
null
);
/** 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 () => {
if (session.none) {
setInitialized(true);
setUserSession(undefined);
setUserSession(null);
} else {
const { accessToken, email } = session.val;

View File

@ -148,7 +148,7 @@ function loadAmplifyConfig(
return {
...baseConfig,
...platformConfig,
urlOpener,
...(urlOpener ? { urlOpener } : {}),
};
}
@ -225,13 +225,14 @@ function handleAuthResponse(url: string) {
try {
/** # 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
* 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
* 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
await (amplify.Auth as any)._handleAuthResponse(url);
// @ts-expect-error `_handleAuthResponse` is a private method without typings.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
await amplify.Auth._handleAuthResponse(url);
} finally {
/** Restore the original `window.location.replaceState` function. */
window.history.replaceState = replaceState;

View File

@ -11,13 +11,11 @@
/** Path data for the SVG icons used in app. */
export const PATHS = {
/** Path data for the `@` icon SVG. */
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 " +
"8.959 0 01-4.5 1.207",
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 \
8.959 0 01-4.5 1.207`,
/** Path data for the lock icon SVG. */
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 " +
"0 00-8 0v4h8z",
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 \
0 00-8 0v4h8z`,
/** 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",
/** Path data for the "create account" icon SVG. */

View File

@ -8,6 +8,7 @@
* included in the final bundle. */
// It is safe to disable `no-restricted-syntax` because the `PascalCase` naming is required
// 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
import * as React from "react";
import * as reactDOM from "react-dom/client";

View File

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

View File

@ -29,7 +29,7 @@ export function brand<T extends Brand<string>>(s: NoBrand & Omit<T, '$brand'>):
// branded type, even if that value is not an instance of the branded type. For example, the
// string "foo" could be cast to the `UserPoolId` branded type, although this string is clearly
// not a valid `UserPoolId`. This is acceptable because the branded type is only used to prevent
// accidental misuse of values, and not to enforce correctness. That is, it is up to the
// accidental misuse of values, and not to enforce correctness. That is, it is up to the
// programmer to declare the correct type of a value. After that point, it is up to the branded
// type to keep that guarantee by preventing accidental misuse of the value.
// eslint-disable-next-line no-restricted-syntax

View File

@ -146,67 +146,67 @@ async function genIcons(outputDir) {
return
} else {
console.log(`Generating icons to ${outputDir}`)
}
console.log('Generating SVG icons.')
await fs.mkdir(path.resolve(outputDir, 'svg'), { recursive: true })
await fs.mkdir(path.resolve(outputDir, 'png'), { recursive: true })
for (const size of sizes) {
let name = `icon_${size}x${size}.svg`
let logo = new Logo(size, true).generate()
await fs.writeFile(`${outputDir}/svg/${name}`, logo)
}
console.log('Generating SVG icons.')
await fs.mkdir(path.resolve(outputDir, 'svg'), { recursive: true })
await fs.mkdir(path.resolve(outputDir, 'png'), { recursive: true })
for (const size of sizes) {
let name = `icon_${size}x${size}.svg`
let logo = new Logo(size, true).generate()
await fs.writeFile(`${outputDir}/svg/${name}`, logo)
}
/// Please note that this function converts the SVG to PNG
/// AND KEEPS THE METADATA INFORMATION ABOUT DPI OF 144.
/// It is required to properly display png images on MacOS.
/// There is currently no other way in `sharp` to do it.
console.log('Generating PNG icons.')
for (const size of sizes) {
let inName = `icon_${size}x${size}.svg`
let outName = `icon_${size}x${size}.png`
await sharp(`${outputDir}/svg/${inName}`, { density: MACOS_DPI })
.png()
.resize({
width: size,
kernel: sharp.kernel.mitchell,
})
.toFile(`${outputDir}/png/${outName}`)
}
/// Please note that this function converts the SVG to PNG
/// AND KEEPS THE METADATA INFORMATION ABOUT DPI OF 144.
/// It is required to properly display png images on MacOS.
/// There is currently no other way in `sharp` to do it.
console.log('Generating PNG icons.')
for (const size of sizes) {
let inName = `icon_${size}x${size}.svg`
let outName = `icon_${size}x${size}.png`
await sharp(`${outputDir}/svg/${inName}`, { density: MACOS_DPI })
.png()
.resize({
width: size,
kernel: sharp.kernel.mitchell,
})
.toFile(`${outputDir}/png/${outName}`)
}
for (const size of sizes.slice(1)) {
let size2 = size / 2
let inName = `icon_${size}x${size}.svg`
let outName = `icon_${size2}x${size2}@2x.png`
await sharp(`${outputDir}/svg/${inName}`, { density: MACOS_DPI })
.png()
.resize({
width: size,
kernel: sharp.kernel.mitchell,
})
.toFile(`${outputDir}/png/${outName}`)
}
for (const size of sizes.slice(1)) {
let size2 = size / 2
let inName = `icon_${size}x${size}.svg`
let outName = `icon_${size2}x${size2}@2x.png`
await sharp(`${outputDir}/svg/${inName}`, { density: MACOS_DPI })
.png()
.resize({
width: size,
kernel: sharp.kernel.mitchell,
})
.toFile(`${outputDir}/png/${outName}`)
}
if (os.platform() === 'darwin') {
console.log('Generating ICNS.')
childProcess.execSync(`cp -R ${outputDir}/png ${outputDir}/png.iconset`)
childProcess.execSync(
`iconutil --convert icns --output ${outputDir}/icon.icns ${outputDir}/png.iconset`
)
}
if (os.platform() === 'darwin') {
console.log('Generating ICNS.')
childProcess.execSync(`cp -R ${outputDir}/png ${outputDir}/png.iconset`)
childProcess.execSync(
`iconutil --convert icns --output ${outputDir}/icon.icns ${outputDir}/png.iconset`
)
}
console.log('Generating ICO.')
let files = []
for (const size of winSizes) {
let inName = `icon_${size}x${size}.png`
let data = await fs.readFile(`${outputDir}/png/${inName}`)
files.push(data)
}
const icoBuffer = await toIco(files)
fsSync.writeFileSync(`${outputDir}/icon.ico`, icoBuffer)
console.log('Generating ICO.')
let files = []
for (const size of winSizes) {
let inName = `icon_${size}x${size}.png`
let data = await fs.readFile(`${outputDir}/png/${inName}`)
files.push(data)
let handle = await fs.open(donePath, 'w')
await handle.close()
return
}
const icoBuffer = await toIco(files)
fsSync.writeFileSync(`${outputDir}/icon.ico`, icoBuffer)
let handle = await fs.open(donePath, 'w')
await handle.close()
}
/** 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.
*
* @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 }) {
assets = assets ?? path.join(root, 'assets')

View File

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

View File

@ -48,6 +48,8 @@ declare module 'create-servers' {
}
export default function (
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
): unknown
}

View File

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

View File

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