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 39 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a52d312
apiClient: update extension to ts
clairep94 Jul 26, 2025
2480bd1
apiClient: add axios instance type
clairep94 Jul 26, 2025
87adc72
device: update extension to ts
clairep94 Jul 26, 2025
9efdd25
device: add test
clairep94 Jul 26, 2025
462524c
eslint: remove prefer-default-export rule
clairep94 Jul 26, 2025
8010af7
device.ts: add edgecase test and refactor
clairep94 Jul 26, 2025
cb9ddc5
metaKey.js: update extention to ts
clairep94 Jul 26, 2025
f7282c2
metaKey: update to use isMac() -- due to navigator.platform being dep…
clairep94 Jul 26, 2025
411eb4a
useKeyDownHandler: refactor to use isMac()
clairep94 Jul 26, 2025
e7eaa2c
language-utils.js: update ext to ts
clairep94 Jul 26, 2025
53962db
language-utils.ts: add test
clairep94 Jul 26, 2025
3efc164
language-utils: add types, passes typecheck
clairep94 Jul 26, 2025
fa6d69f
language-utils: refactor for clarity & disable eslint noplusplus rule
clairep94 Jul 26, 2025
75b35a3
formatDate: update ext to ts
clairep94 Jul 26, 2025
b7fb7a0
formatDate.js: add test
clairep94 Jul 26, 2025
2440d6f
formatDate.js: add types and refactor
clairep94 Jul 26, 2025
0a9af47
consoleUtils: update extension to ts
clairep94 Jul 26, 2025
5302f54
consoleUtils: add tests and bare minimum type
clairep94 Jul 26, 2025
2638c32
consoleUtils.ts: add jsdocs and return type
clairep94 Jul 26, 2025
723cac3
dispatcher.js: update extention to ts --no-verify
clairep94 Jul 26, 2025
e0cb6c7
dispatcher: add unit test
clairep94 Jul 26, 2025
0aab549
dispatcher.ts: update with types
clairep94 Jul 26, 2025
60f8a6e
dispatcher.ts: add jsdocs
clairep94 Jul 26, 2025
04ca29a
remove jsdocs on internal functions to retain git history?
clairep94 Jul 26, 2025
e91000f
evaluateExpression: update ext to ts --no-verify
clairep94 Jul 26, 2025
0713173
evaluateExpression: add unit test
clairep94 Jul 26, 2025
16d84e1
evaluateExpression: add tests
clairep94 Jul 26, 2025
6957124
reduxFormUtils: update ext to ts --no-verify
clairep94 Jul 26, 2025
97754db
reduxFormUtils: add unit test, no-verify
clairep94 Jul 26, 2025
ed0c6f0
reduxFormUtils: delete unused dom-onlyprops function
clairep94 Jul 26, 2025
0b223df
reduxFormUtils: add types and jsdocs
clairep94 Jul 26, 2025
a52372d
getConfig.js: change to ts, no-verify
clairep94 Jul 26, 2025
de6fc10
getConfig.ts: remove circular logic for env check and add types
clairep94 Jul 26, 2025
8061e09
migrate getConfig.test to ts
clairep94 Jul 26, 2025
058d155
update tests for get config after typing
clairep94 Jul 26, 2025
34f0735
add parseStringToType util
clairep94 Jul 26, 2025
c6c7d40
update parsers and update useages of getConfig
clairep94 Jul 26, 2025
2362807
update formatDate to ts, fix type errors
clairep94 Jul 26, 2025
1f5614c
Merge branch 'develop' into pr05/migrate_client_utils_folder
clairep94 Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"tsx": "never"
}
],
"import/prefer-default-export": "off",
"comma-dangle": 0, // not sure why airbnb turned this on. gross!
"default-param-last": 0,
"no-else-return" :0,
Expand All @@ -40,6 +41,7 @@
"no-restricted-exports": 1,
"no-underscore-dangle": 0,
"no-useless-catch": 2,
"no-plusplus": "off",
"prefer-object-spread": 0,
"max-len": [1, 120, 2, {"ignoreComments": true, "ignoreTemplateLiterals": true}],
"max-classes-per-file": 0,
Expand Down
4 changes: 2 additions & 2 deletions client/common/useKeyDownHandlers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mapKeys } from 'lodash';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useRef } from 'react';
import { isMac } from '../utils/device';

/**
* Attaches keydown handlers to the global document.
Expand Down Expand Up @@ -30,8 +31,7 @@ export default function useKeyDownHandlers(keyHandlers) {
*/
const handleEvent = useCallback((e) => {
if (!e.key) return;
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
const isCtrl = isMac ? e.metaKey : e.ctrlKey;
const isCtrl = isMac() ? e.metaKey : e.ctrlKey;
if (e.shiftKey && isCtrl) {
handlers.current[
`ctrl-shift-${
Expand Down
4 changes: 3 additions & 1 deletion client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 Author

Choose a reason for hiding this comment

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

updated this to return a '' if no API_URL is found --> used in ln261 as a template literal so it was previously doing

const win = window.open(`${undefined}/projects/${projectId}/zip`, '_blank');

which would give:

window.open(`undefined/projects/${projectId}/zip`, '_blank'))

const S3_BUCKET_URL_BASE = getConfig('S3_BUCKET_URL_BASE');
const S3_BUCKET = getConfig('S3_BUCKET');

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

Choose a reason for hiding this comment

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

Added this check for safety because I think str.includes(undefined) runs the same as str.includes("undefined")
so

"foo undefined bar".includes(undefined); // true

(file.url.includes(S3_BUCKET_URL_BASE) ||
file.url.includes(S3_BUCKET))
) {
Expand Down
8 changes: 5 additions & 3 deletions client/modules/IDE/actions/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { handleCreateFile } from './files';

export const s3BucketHttps =
getConfig('S3_BUCKET_URL_BASE') ||
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig(
'S3_BUCKET'
)}/`;
`https://s3-${getConfig('AWS_REGION', {
nullishString: true
})}.amazonaws.com/${getConfig('S3_BUCKET', {
nullishString: true
})}/`;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB

function isS3Upload(file) {
Expand Down
4 changes: 2 additions & 2 deletions client/modules/IDE/components/AssetSize.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import { useSelector } from 'react-redux';
import prettyBytes from 'pretty-bytes';

import getConfig from '../../../utils/getConfig';
import { parseNumber } from '../../../utils/parseStringToType';

const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limit = parseNumber(getConfig('UPLOAD_LIMIT')) || 250000000;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Use the new parseNumber utility to transform the string from getConfig to a number

Currently this file is still jsx so it would be ok to use the string version of this number, but it's not as safe

const MAX_SIZE_B = limit;

const formatPercent = (percent) => {
Expand Down
19 changes: 12 additions & 7 deletions client/modules/IDE/components/Header/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MenubarSubmenu from '../../../../components/Menubar/MenubarSubmenu';
import MenubarItem from '../../../../components/Menubar/MenubarItem';
import { availableLanguages, languageKeyToLabel } from '../../../../i18n';
import getConfig from '../../../../utils/getConfig';
import { parseBoolean } from '../../../../utils/parseStringToType';
import { showToast } from '../../actions/toast';
import { setLanguage } from '../../actions/preferences';
import Menubar from '../../../../components/Menubar/Menubar';
Expand Down Expand Up @@ -80,8 +81,14 @@ LeftLayout.defaultProps = {
layout: 'project'
};

const isLoginEnabled = parseBoolean(getConfig('LOGIN_ENABLED'), true);
const isUiCollectionsEnabled = parseBoolean(
getConfig('UI_COLLECTIONS_ENABLED'),
true
);
const isExamplesEnabled = parseBoolean(getConfig('EXAMPLES_ENABLED'), true);

const UserMenu = () => {
const isLoginEnabled = getConfig('LOGIN_ENABLED');
const isAuthenticated = useSelector(getAuthenticated);

if (isLoginEnabled && isAuthenticated) {
Expand Down Expand Up @@ -177,7 +184,7 @@ const ProjectMenu = () => {
id="file-save"
isDisabled={
!user.authenticated ||
!getConfig('LOGIN_ENABLED') ||
!isLoginEnabled ||
(project?.owner && !isUserOwner)
}
onClick={() => saveSketch(cmRef.current)}
Expand Down Expand Up @@ -216,17 +223,15 @@ const ProjectMenu = () => {
<MenubarItem
id="file-add-to-collection"
isDisabled={
!getConfig('UI_COLLECTIONS_ENABLED') ||
!user.authenticated ||
isUnsaved
!isUiCollectionsEnabled || !user.authenticated || isUnsaved
}
href={`/${user.username}/sketches/${project?.id}/add-to-collection`}
>
{t('Nav.File.AddToCollection')}
</MenubarItem>
<MenubarItem
id="file-examples"
isDisabled={!getConfig('EXAMPLES_ENABLED')}
isDisabled={!isExamplesEnabled}
href="/p5/sketches"
>
{t('Nav.File.Examples')}
Expand Down Expand Up @@ -370,7 +375,7 @@ const AuthenticatedUserMenu = () => {
<MenubarItem
id="account-collections"
href={`/${username}/collections`}
isDisabled={!getConfig('UI_COLLECTIONS_ENABLED')}
isDisabled={!isUiCollectionsEnabled}
>
{t('Nav.Auth.MyCollections')}
</MenubarItem>
Expand Down
2 changes: 1 addition & 1 deletion client/modules/IDE/components/SketchListRowBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import MenuItem from '../../../components/Dropdown/MenuItem';
import dates from '../../../utils/formatDate';
import getConfig from '../../../utils/getConfig';

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

const formatDateCell = (date, mobile = false) =>
dates.format(date, { showTime: !mobile });
Expand Down
3 changes: 2 additions & 1 deletion client/modules/IDE/components/UploadFileModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import { closeUploadFileModal } from '../actions/ide';
import FileUploader from './FileUploader';
import { getreachedTotalSizeLimit } from '../selectors/users';
import Modal from './Modal';
import { parseNumber } from '../../../utils/parseStringToType';

const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limit = parseNumber(getConfig('UPLOAD_LIMIT')) || 250000000;
const limitText = prettyBytes(limit);

const UploadFileModal = () => {
Expand Down
3 changes: 2 additions & 1 deletion client/modules/IDE/selectors/users.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createSelector } from '@reduxjs/toolkit';
import getConfig from '../../../utils/getConfig';
import { parseNumber } from '../../../utils/parseStringToType';

export const getAuthenticated = (state) => state.user.authenticated;
const getTotalSize = (state) => state.user.totalSize;
const getAssetsTotalSize = (state) => state.assets.totalSize;
export const getSketchOwner = (state) => state.project.owner;
const getUserId = (state) => state.user.id;
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
const limit = parseNumber(getConfig('UPLOAD_LIMIT')) || 250000000;

export const getCanUploadMedia = createSelector(
getAuthenticated,
Expand Down
10 changes: 6 additions & 4 deletions client/modules/Preview/EmbedFrame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`
}

const previewScripts = sketchDoc.createElement('script');
previewScripts.src = `${window.location.origin}${getConfig(
'PREVIEW_SCRIPTS_URL'
)}`;
previewScripts.src = `${window.location.origin}${
(getConfig('PREVIEW_SCRIPTS_URL'), { nullishString: true })
}`;
previewScripts.setAttribute('crossorigin', '');
sketchDoc.head.appendChild(previewScripts);

Expand All @@ -245,7 +245,9 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);`
window.offs = ${JSON.stringify(scriptOffs)};
window.objectUrls = ${JSON.stringify(objectUrls)};
window.objectPaths = ${JSON.stringify(objectPaths)};
window.editorOrigin = '${getConfig('EDITOR_URL')}';
window.editorOrigin = '${
(getConfig('EDITOR_URL'), { nullishString: true })
}';
`;
addLoopProtect(sketchDoc);
sketchDoc.head.prepend(consoleErrorsScript);
Expand Down
7 changes: 3 additions & 4 deletions client/utils/apiClient.js → client/utils/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import axios from 'axios';

import axios, { AxiosInstance } from 'axios';
import getConfig from './getConfig';

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

/**
* Configures an Axios instance with the correct API URL
*/
function createClientInstance() {
function createClientInstance(): AxiosInstance {
return axios.create({
baseURL: ROOT_URL,
withCredentials: true
Expand Down
85 changes: 85 additions & 0 deletions client/utils/consoleUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { getAllScriptOffsets, startTag } from './consoleUtils';

describe('getAllScriptOffsets', () => {
// not sure how the line offset calculations have been formulated
it('returns an empty array when no scripts are found', () => {
const html = '<html><body><h1>No scripts here</h1></body></html>';
expect(getAllScriptOffsets(html)).toEqual([]);
});

it('detects a single external script with @fs- path', () => {
const html = `
<html>
<head>
<script src="${startTag}my-script.js"></script>
</head>
</html>
`;
const result = getAllScriptOffsets(html);
expect(result.every(([offset, _]) => typeof offset === 'number')).toBe(
true
);
expect(result.map(([_, name]) => name)).toEqual(['my-script']);
});

it('detects multiple external scripts with @fs- paths', () => {
const html = `
<script src="${startTag}one.js"></script>
<script src="${startTag}two.js"></script>
`;
const result = getAllScriptOffsets(html);
expect(result.every(([offset, _]) => typeof offset === 'number')).toBe(
true
);
expect(result.map(([_, name]) => name)).toEqual(['one', 'two']);
});

it('detects embedded scripts with crossorigin attribute', () => {
const html = `
<html>
<head>
<script crossorigin=""></script>
</head>
</html>
`;
const result = getAllScriptOffsets(html);
expect(result.every(([offset, _]) => typeof offset === 'number')).toBe(
true
);
expect(result.map(([_, name]) => name)).toEqual(['index.html']);
});

it('detects both @fs- scripts and embedded scripts together, ordering embedded scripts last', () => {
const html = `
<script src="${startTag}abc.js"></script>
<script crossorigin=""></script>
<script src="${startTag}xyz.js"></script>
`;
const result = getAllScriptOffsets(html);
expect(result.every(([offset, _]) => typeof offset === 'number')).toBe(
true
);
expect(result.map(([_, name]) => name)).toEqual([
'abc',
'xyz',
'index.html'
]);
});

it('handles scripts with varying whitespace and newlines', () => {
const html = `
<script src="${startTag}some-script.js">
</script>
<script crossorigin="">
</script>
`;
const result = getAllScriptOffsets(html);
expect(result.every(([offset, _]) => typeof offset === 'number')).toBe(
true
);
expect(result.map(([_, name]) => name)).toEqual([
'some-script',
'index.html'
]);
});
});
11 changes: 9 additions & 2 deletions client/utils/consoleUtils.js → client/utils/consoleUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
export const startTag = '@fs-';

export const getAllScriptOffsets = (htmlFile) => {
const offs = [];
export type ScriptOffset = [number, string];

/**
* Extracts line offsets and filenames for JS scripts embedded in an HTML string.
* @param htmlFile - Full HTML file content as a string
* @returns Array of [lineOffset, filename] pairs
*/
export const getAllScriptOffsets = (htmlFile: string): ScriptOffset[] => {
const offs: ScriptOffset[] = [];
const hijackConsoleErrorsScriptLength = 2;
const embeddedJSStart = 'script crossorigin=""';
let foundJSScript = true;
Expand Down
1 change: 0 additions & 1 deletion client/utils/device.js

This file was deleted.

45 changes: 45 additions & 0 deletions client/utils/device.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isMac } from './device';

describe('isMac', () => {
const originalUserAgent = navigator.userAgent;

afterEach(() => {
// Restore the original userAgent after each test
Object.defineProperty(navigator, 'userAgent', {
value: originalUserAgent,
configurable: true
});
});

it('returns true when userAgent contains "Mac"', () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
configurable: true
});
expect(isMac()).toBe(true);
});

it('returns false when userAgent does not contain "Mac"', () => {
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
configurable: true
});
expect(isMac()).toBe(false);
});

it('returns false when navigator agent is null', () => {
Object.defineProperty(navigator, 'userAgent', {
value: null,
configurable: true
});
expect(isMac()).toBe(false);
});

it('returns false when navigator agent is undefined', () => {
Object.defineProperty(navigator, 'userAgent', {
value: undefined,
configurable: true
});
expect(isMac()).toBe(false);
});
});
10 changes: 10 additions & 0 deletions client/utils/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Checks if the user's OS is macOS based on the `navigator.userAgent` string.
* This is the preferred method over `navigator.platform`, which is now deprecated:
* - see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform
*/
export function isMac(): boolean {
return typeof navigator?.userAgent === 'string'
? navigator.userAgent.toLowerCase().includes('mac')
: false;
}
Loading