Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better TS #302

Merged
merged 15 commits into from
Nov 8, 2024
Merged
77 changes: 61 additions & 16 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-check
// TODO(@lishaduck) [eslint@>9.5]: Use `.ts` extension to get more type checking for this file.
// TODO(@lishaduck) [eslint@>=9.9]: Use `.ts` extension to get more type checking for this file.
// TODO(@lishaduck) [engine:node@>=18]: Upgrade `tseslint`.
// TODO(@lishaduck) [engine:node@>=18]: Use `eslint-define-config` to get type checking for this file.
// TODO(@lishaduck) [engine:node@>=18]: Use `eslint-plugin-jsdoc` to get JSDoc linting.
Expand Down Expand Up @@ -28,6 +28,13 @@ module.exports = {
],
parser: '@typescript-eslint/parser',
parserOptions: {
// Ensure JSDoc parsing is enabled.
jsDocParsingMode: 'all',

// Speed up ESLint CLI runs. This is opt-out in v8.
// The only known bugs are with project references, which we don't use.
automaticSingleRunInference: true,

// A stable, but experimental, option to speed up linting.
// It's also more feature complete, as it relies on the TypeScript Language Service.
EXPERIMENTAL_useProjectService: true // TODO(@lishaduck) [typescript-eslint@>=8]: Rename to `projectService`.
Expand Down Expand Up @@ -72,7 +79,18 @@ module.exports = {
'unicorn/prefer-node-protocol': 'error',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'default-case': 'off',
Expand All @@ -99,20 +117,38 @@ module.exports = {
'unicorn/import-style': ['off'], // TODO(@lishaduck): Re-enable this once we use ESM.
'unicorn/no-null': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/catch-error-name': ['error', {ignore: [/^err/i]}], // We use "error" for the result of `intoError` as well.
'no-fallthrough': 'off', // TSESLint doesn't provide an alternative, and TS checks for this anyway.
'no-void': ['error', {allowAsStatement: true}],
'@typescript-eslint/no-misused-promises': [
'error',
{
// TODO(@lishaduck): Enable stricter promise rules.
checksVoidReturn: {
returns: false,
arguments: false
}
}
],

// `typescript-eslint` v8, but now:
'@typescript-eslint/no-array-delete': 'error', // Recommended in v8
'no-loss-of-precision': 'error', // This rule handles numeric separators now
'@typescript-eslint/no-loss-of-precision': 'off', // This rule is redundant
'no-unused-expressions': 'off', // This rule is replaced with the TSESlint version.
'@typescript-eslint/no-unused-expressions': 'error', // Support TS stuff
'@typescript-eslint/no-throw-literal': 'error', // Recommended in v8 (w/rename to `only-throw-error`)
'@typescript-eslint/prefer-find': 'error', // Recommended in v8
'@typescript-eslint/prefer-includes': 'error', // Recommended in v8
'@typescript-eslint/prefer-regexp-exec': 'error', // Recommended in v8
'prefer-promise-reject-errors': 'off', // TSESlint provides an alternative
'@typescript-eslint/prefer-promise-reject-errors': 'error', // Recommended in v8

// Unsafe
'@typescript-eslint/no-unsafe-assignment': 'off', // Blocked on typescript-eslint/typescript-eslint#1682.
// TODO(@lishaduck): Once there are no more `any`s, start enforcing these rules.
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',

// TODO(@lishaduck): Enable stricter promise rules.
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'promise/catch-or-return': 'off',
'promise/always-return': 'off',

// TODO(@lishaduck): Security issues that should eventually get fixed.
'security/detect-object-injection': 'off',
Expand All @@ -125,20 +161,29 @@ module.exports = {
'unicorn/prefer-at': 'off' // TODO(@lishaduck) [engine:node@>=16.6]: Enable this rule.
},
overrides: [
{
files: ['./*.js'],
rules: {
// Not compatible with JSDoc according https://github.com/typescript-eslint/typescript-eslint/issues/8955#issuecomment-2097518639
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/parameter-properties': 'off',
'@typescript-eslint/typedef': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off'
}
},
{
files: ['./new-package/**/*.js'],
rules: {
'n/no-process-exit': 'off',
'n/no-missing-require': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/unbound-method': 'off' // TODO(@lishaduck): Fix this warning. I just got confused.
'n/no-missing-require': 'off', // `require` of `elm.json`.
'@typescript-eslint/ban-ts-comment': 'off' // `require` of `elm.json`.
}
},
{
files: ['.eslintrc.js'],
rules: {
camelcase: 'off',
'@typescript-eslint/prefer-nullish-coalescing': 'off' // It's not happy with how TS is set up on this config file.
camelcase: 'off'
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion lib/anonymize.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* This module aims to make the paths and versions used in the CLI generic
* @file This module aims to make the paths and versions used in the CLI generic,
* so that the CLI tests (in the `test/` folder) have the same output on different
* machines, and also the same output when only the CLI version changes.
*/
Expand Down
23 changes: 15 additions & 8 deletions lib/app-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/**
* @import {ReviewApp} from './types/app'
* @import {WorkerData} from './types/elm-app-wrapper';
* @import {Flags} from './types/flags'
* @import {Options} from './types/options'
* @import {Path} from './types/path'
* @import {CallbackFn, Listened, Port} from './types/promisify-port'
* @import {Flags} from './types/flags'
* @import {App} from './types/app'
*/
const path = require('node:path');
const {Worker} = require('node:worker_threads');
Expand All @@ -13,7 +15,7 @@ const loadCompiledElmApp = require('./load-compiled-app');
* @param {Options} options
* @param {string} elmModulePath
* @param {Flags} flags
* @returns {App}
* @returns {ReviewApp}
*/
function init(options, elmModulePath, flags) {
if (options.watch) {
Expand Down Expand Up @@ -50,16 +52,16 @@ const elmPortsInterfaceProxy = {
};

/**
* @param {string} elmModulePath
* @param {Path} elmModulePath
* @param {Flags} flags
* @returns {App}
* @returns {ReviewApp}
*/
function initWithWorker(elmModulePath, flags) {
worker = new Worker(pathToWorker, {
workerData: {
workerData: /** @satisfies {WorkerData} */ ({
elmModulePath,
flags
}
})
});

worker.on(
Expand All @@ -79,6 +81,7 @@ function initWithWorker(elmModulePath, flags) {

/**
* @param {string | symbol} port
* @returns {Listened<unknown>}
*/
function send(port) {
return (/** @type {unknown} */ data) => {
Expand Down Expand Up @@ -135,6 +138,7 @@ function initializeListeners() {
/**
* @param {string} elmModulePath
* @param {Flags} flags
* @returns {ReviewApp}
*/
function initWithoutWorker(elmModulePath, flags) {
const elmModule = loadCompiledElmApp(elmModulePath);
Expand All @@ -144,9 +148,12 @@ function initWithoutWorker(elmModulePath, flags) {
return app;
}

/**
* @returns {void}
*/
function stop() {
if (worker) {
worker.terminate();
void worker.terminate();
worker = null;
listeners = initializeListeners();
}
Expand Down
13 changes: 8 additions & 5 deletions lib/autofix.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* @import {App, AutofixRequest} from './types/app';
* @import {ReviewApp, AutofixRequest} from './types/app';
* @import {File} from './types/content';
* @import {VersionString} from './types/version';
* @import {Options} from './types/options';
* @import {Path} from './types/path';
* @import {Listened} from './types/promisify-port';
* @import {FilesProposedByCurrentFix} from './types/state';
*/
const fs = require('node:fs');
Expand All @@ -25,8 +26,9 @@ const StyledMessage = require('./styled-message');
* Subscribe to fix requests to prompt the user.
*
* @param {Options} options
* @param {App} app
* @param {ReviewApp} app
* @param {VersionString} elmVersion
* @returns {void}
*/
function subscribe(options, app, elmVersion) {
AppState.subscribe(
Expand All @@ -39,9 +41,9 @@ function subscribe(options, app, elmVersion) {
* Subscribe to fix requests to prompt the user.
*
* @param {Options} options
* @param {App} app
* @param {ReviewApp} app
* @param {VersionString} elmVersion
* @returns {(data: AutofixRequest) => Promise<void | never>}
* @returns {Listened<AutofixRequest>}
*/
function askConfirmationToFixWithOptions(options, app, elmVersion) {
return async (data) => {
Expand Down Expand Up @@ -146,6 +148,7 @@ function askConfirmationToFixWithOptions(options, app, elmVersion) {
* @returns {Promise<boolean>}
*/
async function confirmFix(message) {
/** @type {{accepted: boolean}} */
const prompt = await prompts({
type: 'confirm',
name: 'accepted',
Expand All @@ -157,7 +160,7 @@ async function confirmFix(message) {
}

/**
* @param {App} app
* @param {ReviewApp} app
* @returns {PromiseLike<boolean>}
*/
function checkIfAFixConfirmationIsStillExpected(app) {
Expand Down
19 changes: 12 additions & 7 deletions lib/build.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* @import {CompileOptions, Sources} from '../vendor/types/node-elm-compiler';
* @import {AppHash, BuildResult} from './types/build';
* @import {ApplicationElmJson, ElmJson} from './types/content';
* @import {VersionString} from './types/version';
* @import {ErrorMessageInfo} from './types/error-message';
* @import {Options, Template} from './types/options';
* @import {Path} from './types/path';
* @import {CompileOptions, Sources} from '../vendor/types/node-elm-compiler';
* @import {VersionString} from './types/version';
*/
const path = require('node:path');
const crypto = require('node:crypto');
Expand Down Expand Up @@ -334,6 +335,7 @@ async function createTemplateProject(
* @param {Options} options
* @param {string} userSrc
* @param {ApplicationElmJson} elmJson
* @returns {ApplicationElmJson}
*/
function updateSourceDirectories(options, userSrc, elmJson) {
let sourceDirectories = [
Expand All @@ -359,10 +361,10 @@ function updateSourceDirectories(options, userSrc, elmJson) {

/**
* @param {Options} options
* @param {string} dest
* @param {string} elmModulePath
* @param {Path} dest
* @param {Path} elmModulePath
* @param {Sources} compileTargets
* @param {string} elmBinary
* @param {Path} elmBinary
* @param {boolean} isReviewApp
* @returns {Promise<string | null>}
*/
Expand Down Expand Up @@ -455,6 +457,7 @@ async function compileElmProject(
/**
* @param {Options} options
* @param {string} stderr
* @returns {ErrorMessageInfo}
*/
function compilationError(options, stderr) {
if (stderr.includes('DEBUG REMNANTS')) {
Expand Down Expand Up @@ -496,6 +499,7 @@ function compilationError(options, stderr) {
* @param {Options} options
* @param {Path} reviewElmJsonPath
* @param {ElmJson} reviewElmJson
* @returns {void}
*/
function validateElmReviewVersion(options, reviewElmJsonPath, reviewElmJson) {
if (options.localElmReviewSrc) {
Expand Down Expand Up @@ -580,12 +584,13 @@ async function buildElmParser(options, reviewElmJson) {
// Needed when the user has `"type": "module"` in their package.json.
// Our output is CommonJS.
// TODO(@lishaduck): Switch to a .cjs file instead.
FS.mkdirp(options.generatedCodePackageJson()).then(async () => {
(async () => {
await FS.mkdirp(options.generatedCodePackageJson());
await FS.writeFile(
path.join(options.generatedCodePackageJson(), 'package.json'),
'{"type":"commonjs"}'
);
})
})()
]);

await compileElmProject(
Expand Down
2 changes: 1 addition & 1 deletion lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function getOrCompute(folder, key, fn) {

const result = await fn();
if (result !== null) {
cacheJsonFile(filepath, result);
void cacheJsonFile(filepath, result);
}

return result;
Expand Down
61 changes: 61 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Port of `elm/core` primitives to TS.
* Enables functional-style programming without having to pull in `Effect.TS`.
*/

/**
* @import {Err, Ok, Result} from './types/core';
*/

const {intoError} = require('./utils');

const result = {
/**
* Create a successful result.
*
* @template Value
*
* @param {Value} value
* @returns {Ok<Value>}
*/
succeed(value) {
return {tag: 'ok', value};
},

/**
* Create a failed result.
*
* @template Error
*
* @param {Error} error
* @returns {Err<Error>}
*/
fail(error) {
return {tag: 'err', error};
},

/**
* Returns the value of a result, or throws if in an errored state.
*
* @remarks
* Converts errors into {@linkcode Error}s before throwing.
* For more details, see {@linkcode intoError}.
*
* @template Value
*
* @param {Result<unknown, Value>} value
* @returns {Value}
* @throws {Error}
*/
orThrow(value) {
if (value.tag === 'ok') {
return value.value;
}

throw intoError(value.error);
}
};

module.exports = {
result
};
Loading