Skip to content

pr05 Typescript #3: Migrate client/utils folder #3553

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

Open
wants to merge 40 commits into
base: develop
Choose a base branch
from

Conversation

clairep94
Copy link
Collaborator

@clairep94 clairep94 commented Jul 26, 2025

pr05 Typescript Migration 3: Migrate the client/utils folder

IMPORTANT: Should be reviewed after #3540 for clarity

Context:

  • Rebuild of Pr05/migrate client utils clairep94/p5.js-web-editor#5 --> forgot to do git mv for the first few files and directly renamed, so this looks like deleting and creating a new file for git, and erases the previous history.

  • Migrate as much of the client/utils folder with the following steps:

  1. git mv someUtil.js someUtil.ts. If possible, commit without the no-verify flag, otherwise with no-verify.
    I forgot to add no-verify to my commit message in some of these initial ones
  2. Add unit test to secure current behaviour
  3. Check for all instances of useage for the utility function to determine types & edgecases of types
  4. Add types & handle edgecases & update test
  5. Refactor if beneficial
  6. Add JSDocs
  • I skipped the intellisense autogen files & the codemirror utility files due to unfamiliarity, but migrated most of the rest of the folder

Changes:

Please see annotations on files

  • apiClient
  • device
  • metaKey
  • language-utils
  • formatDate
  • consoleUtils
  • dispatcher
  • evaluateExpression
  • reduxFormUtils
  • getConfig (+ many instances of useage throughout the client folder)
  • NEW: parseStringToType

Notes:

I have verified that this pull request:

  • has no linting errors (npm run lint)
  • has no test errors (npm run test)
  • is from a uniquely-named feature branch and is up to date with the develop branch.
  • is descriptively named and links to an issue number, i.e. Fixes #123

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor the portions of using getConfig for readability instead of inline

use new parseBoolean function to parse the string val of getConfig to a boolean instead
I wonder if this file was previously only checking if process.env.LOGIN_ENABLED had a value??

So if the env file looked like:

LOGIN_ENABLED=false
UI_COLLECTIONS_ENABLED=false

getConfig('LOGIN_ENABLED') // "false"
!getConfig('LOGIN_ENABLED') // false??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried my best with this test, I'm not 100% sure how the line offset works, so I didn't test its calculation, just that it should be a number in the tupple that's returned

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR > Files changed view on Github shows that this is a delete + new file, due to too many changes from the original due to refactoring, so let me know if it would be preferable to go back to the unrefactored!

I refactored for readability & handling the edgecase of not having a navigator or navigator.userAgent, but that might be excessive

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, GH shows this in PR mode as a deletion + new file, but it seems the history is still retained when you view git blame:
https://github.com/processing/p5.js-web-editor/blame/2362807016746d3b3798085437c2406db2d31757/client/utils/dispatcher.ts

Kept in the comments from a previous maintainer & added JSDocs. I think the number of new lines of jsdocs made it pass the diff tolerated percentage before github shows it as a file deletion instead of a file update

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes other than adding types & no refactors on this one

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure if these tests match the function's useage on previewEntry and if there are other cases that need to be added?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small refactor for readability + added JSDocs. History is retained despite looking like a delete + new file here
https://github.com/processing/p5.js-web-editor/blame/2362807016746d3b3798085437c2406db2d31757/client/utils/formatDate.ts

Did small refactor to return '' early if !parsed

Also corrected what I thought was a typo here:
https://github.com/processing/p5.js-web-editor/pull/3553/files#diff-e2130c2674d12d8f2724335d2d8d2b7ade8d366efa4bf42717c2af4eec4fa6aaL28

const diffInMs = differenceInMilliseconds(now, parsed);

      if (Math.abs(diffInMs < 10000)) {
        return i18next.t('formatDate.JustNow');
      } else if (diffInMs < 20000) {
        return i18next.t('formatDate.15Seconds');
      } else if (diffInMs < 30000) {
        return i18next.t('formatDate.25Seconds');
      } else if (diffInMs < 46000) {
        return i18next.t('formatDate.35Seconds');
      }

It seems like they're using Math.abs incorrectly --> it's using Math.abs(true) // 1 and Math.abs(false) // 0

I updated to be :

const diffInMs = Math.abs(differenceInMilliseconds(now, parsed));

// rest of checks for what range the absolute diff between now and the time something happened falls under

Copy link
Collaborator Author

@clairep94 clairep94 Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

History retained here:
https://github.com/processing/p5.js-web-editor/blame/2362807016746d3b3798085437c2406db2d31757/client/utils/getConfig.ts

Refactors:

For when we want a number or boolean I added a new utility file called parseStringToType below

  • I was going to do the parsing within getConfig, as I did with my proof-of-concept PR from my grant application, but I thought actually keeping the parsing in a separate function keeps things more modular and testable & keeps the return types of getConfig consistent for predictability

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

History retained here:
https://github.com/processing/p5.js-web-editor/blame/2362807016746d3b3798085437c2406db2d31757/client/utils/language-utils.ts

Create a unit test to secure all behaviour for this function prior to refactor, then did a refactor to make the long for-loop & nested if-statements more readable

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this old implementation uses navigator.platform which is deprecated:
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform

I updated this file to use utils/device to determine if the client is using a mac

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New utility functions to parse a string (from getConfig) to a specified type:

for parseNumber it returns a number if parseable or undefined/0 depending on the nullishNumber flag

for parseBoolean it returns a boolean if parseable or undefined/false depending on the nullishBoolean flag

Copy link
Collaborator Author

@clairep94 clairep94 Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

History retained here:
https://github.com/processing/p5.js-web-editor/blame/2362807016746d3b3798085437c2406db2d31757/client/utils/reduxFormUtils.ts

  • Moved the username regex to top of file for readability

  • Changed the username length check to .length instead of regex for readability: https://github.com/processing/p5.js-web-editor/pull/3553/files#diff-932633409951987d095611a326d0b6b624bd482b07f1e27a9ca7987f6b52a71bR32

  • Moved the internal helper functions higher, and grouped the public functions together on the bottom of the file

  • Added types -- let me know if this organisation makes sense as this was the most stylistic/open-ended thing I did on the PR

  • Also not sure if we want to have a root/types folder, or if it makes sense to keep types collocated where they are most used, and potentially exported, as I did here?

  • No other refactors in each function internally

@clairep94 clairep94 marked this pull request as ready for review July 28, 2025 12:14
@clairep94
Copy link
Collaborator Author

@khanniie @raclim Ready for review! I tried to annotate the files with comments to provide context / explainations for any refactors, but let me know if we should do a sync-review of this one since it's so big

@khanniie khanniie requested a review from raclim August 1, 2025 16:14
Copy link
Collaborator

@khanniie khanniie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still working through the files but thought I'd sent a first wave!

@@ -15,7 +15,7 @@ import {
} from './ide';
import { clearState, saveState } from '../../../persistState';

const ROOT_URL = getConfig('API_URL');
const ROOT_URL = getConfig('API_URL', { nullishString: true });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do we think the "correct" or expected behavior would be if there was no API_URL? like is "/projects/${projectId}/zip" a valid ROOT_URL or would that cause a different error down the line? or in what cases is it undefined?

* Internal function to retrieve env vars, with no error handling.
* @returns String value of env variable or undefined if not found.
*/
function _getConfig(key: string): string | undefined {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI i know we don't enforce this very consistently but i dont think we are meant to use underscores in naming, as we "follow" the airbnb guide https://github.com/airbnb/javascript?tab=readme-ov-file#naming--leading-underscore

@@ -307,6 +307,8 @@ export function cloneProject(project) {
(file, callback) => {
if (
file.url &&
S3_BUCKET &&
S3_BUCKET_URL_BASE &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call!!

str?: string,
nullishNumber = false
): number | undefined {
if (str == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're letting string be typed as str?, then is undefined also a valid nullish value? should it be the more general if(!str) ?

str?: string,
nullishBool = false
): boolean | undefined {
if (str == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

* - see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform
*/
export function isMac(): boolean {
return typeof navigator?.userAgent === 'string'
Copy link
Collaborator

@khanniie khanniie Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need typeof ? why not something like this:

navigator?.userAgent?.toLowerCase().includes('mac') ?? false

} = {};
let frameIndex = 1;

/** Codesandbox dispatcher message types */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if we can just use typescript enums here instead?

enum MessageTypes = {
START,
STOP,
etc
}

*/
export type Message = {
type: MessageType,
payload?: any
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way to use unknown instead of any ?

function eventListener(e: MessageEvent) {
const { data } = e;

// should also store origin of parent? idk
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume this is leftover from the js file but should we remove the dead code comments?


type EvalInClosureFn = (expr: string) => EvalResult;

function __makeEvaluateExpression(evalInClosure: EvalInClosureFn) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid using underscores for naming functions

function __makeEvaluateExpression(evalInClosure) {
return (expr) =>
type EvalResult = {
result: any,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i haven't looked closely enough at where this is used, but is there any way to avoid using any here as well? can we do unknown and then cast it later after doing some type guard checks?

https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

@@ -19,7 +26,11 @@ function evaluateExpression() {
}
result = (0, eval)(newExpr); // eslint-disable-line
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we change this eslint disable to be more specific to what it's disabling?

return null;
}

export default {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure exactly how this is getting imported but i wonder if we can just export these individually instead of in a dict wrapped as one default? totally understand that it's a holdover from the js file though

Copy link
Collaborator

@khanniie khanniie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still working through the files but have a few more comments!

*/
function _getConfig(key: string): string | undefined {
const env: Record<string, string | undefined> =
(typeof global !== 'undefined' ? global : window)?.process?.env || {};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do global !== undefined ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even

const configSource = global ?? window;
const env = configSource?.process?.env ?? {}

return env[key];
}

type GetConfigOptions = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be an interface? maybe using optional?

https://google.github.io/styleguide/tsguide.html#prefer-optional-over-undefined

return value;
}

export default getConfig;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here can we convert to named export? https://google.github.io/styleguide/tsguide.html#exports

supportedLanguages: string[] = [],
defaultLanguage: string = 'en'
): string | undefined {
if (typeof navigator === 'undefined') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here i think we should be able to do something like if (!navigator) return defaultLanguage

return defaultLanguage;
}

export default getPreferredLanguage;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we put the default export on the function itself here too?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even better... maybe we can make it a named export instead

https://google.github.io/styleguide/tsguide.html#exports

@@ -0,0 +1,156 @@
import i18n from 'i18next';

/* eslint-disable */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are we disabling here? can we be more specific about the rule?

/** Processes form & mutates errors to add any `username` & `email` errors */
function validateUsernameEmail(
formProps: Partial<UsernameAndEmail>,
errors: Partial<FormErrors>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure i understand what happens when we do Partial<Partial<....>> do we need the double partial?


/** Validation for the Account Form */
export function validateSettings(
formProps: Partial<AccountForm>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if AccountFormErrors is already Partial<AccountForm> then can we also just use it for formProps?

export type LoginFormErrors = Partial<LoginForm>;

/** Validation for the Login Form */
export function validateLogin(formProps: Partial<LoginForm>): LoginFormErrors {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, why not reuse LoginFormErrors? or rename it to be more general?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr05 Grant Projects pr05 Grant Projects
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants