Thursday, November 18, 2021

Horizontal scrollbar on textarea when text exceeds textarea's width

This works on Chrome:
white-space: nowrap;

However, the above does not work on Safari. In Safari, use this instead:
white-space: pre;
overflow-x: auto;
word-wrap: normal;

https://stackoverflow.com/questions/657795/how-to-remove-word-wrap-from-textarea/25556711#25556711

Sunday, November 14, 2021

Split keep function. Lookbehind, match, loop

Common code:
function escapeRegExp(text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
Lookbehind:
String.prototype.splitKeep = function (tokens) {
    const escaped = escapeRegExp(tokens);
    return this.split(new RegExp(`(?=[${escaped}])|(?<=[${escaped}])`, "g"));
};
Match. Safari does not support lookbehind, use match approach:
String.prototype.splitKeep = function (tokens) {
    const tokensEscaped = tokens
        .split("")
        .map((s) => escapeRegExp(s))
        .join("|");

    const wordMatch = `[^${escapeRegExp(tokens)}]+`;
    return this.match(new RegExp(tokensEscaped + "|" + wordMatch, "g"));
};
Loop-based approach:
String.prototype.splitKeep = function (tokens) {
    let toPush = "";
    const splitList = [];
    for (const c of this) {
        if (tokens.includes(c)) {
            if (toPush.length > 0) {
                splitList.push(toPush);
            }
            splitList.push(c);
            toPush = "";
            continue;
        }

        toPush += c;
    }

    if (toPush.length > 0) {
        splitList.push(toPush);
    }

    return splitList;
};
Benchmark code:
console.time("test");
for (let i = 0; i < 10000; ++i) {
    "pin1yin1".splitKeep("12345 ");
}
console.timeEnd("test");
Results:
% node splitkeep-lookbehind.js
test: 19.35ms
% node splitkeep-lookbehind.js
test: 18.951ms
% node splitkeep-match.js       
test: 54.635ms
% node splitkeep-match.js
test: 51.998ms
% node splitkeep-loop.js 
test: 14.647ms
% node splitkeep-loop.js
test: 13.035ms

Saturday, October 2, 2021

Magic of 1s and 0s

Instead of this:

const VARIANT_CHAR = /variant of \p{Script=Han}/u;

// 'variant of chineseCharacterHere' sorts last
sort((a, b) =>
    VARIANT_CHAR.test(a.definitions.join(' ')) 
    && VARIANT_CHAR.test(b.definitions.join(' ')) ?
        0
    : VARIANT_CHAR.test(a.definitions.join(' ')) ?
        1
    : VARIANT_CHAR.test(b.definitions.join(' ')) ?
        -1
    :
        0
)  

Just do this:

const VARIANT_CHAR = /variant of \p{Script=Han}/u;

// 'variant of chineseCharacterHere' sorts last
sort((a, b) =>
    VARIANT_CHAR.test(a.definitions.join(' ')) 
    -
    VARIANT_CHAR.test(b.definitions.join(' '))
)

Tuesday, June 8, 2021

safari-web-extension-converter error

$ xcrun safari-web-extension-converter
xcrun: error: unable to find utility "safari-web-extension-converter", not a developer tool or in PATH
Do this first:
sudo xcode-select -s /Applications/Xcode.app

Saturday, May 29, 2021

TypeScript Object is possibly undefined

   .
   .
   .
   
   newHzl[hanzi].pinyin = newHzl[hanzi].pinyin.filter(eachPinyin => pinyinEnglish[eachPinyin]);
} // for loop   
That yields this error:
error: TS2532 [ERROR]: Object is possibly 'undefined'.
        newHzl[hanzi].pinyin = newHzl[hanzi].pinyin.filter(
Got a similar error code on following code, and removed the error by checking if a variable has nothing, if nothing then skip(continue)
for (const [hanzi, { pinyinEnglish }] of Object.entries(hzl)) {
    if (!pinyinEnglish) {
        continue;
    }
    
    for (const pinyin of Object.keys(pinyinEnglish)) {
        if (pinyinEnglish[pinyin].length === 0) {
            delete pinyinEnglish[pinyin];
        }
    }
    
    .
    .
    .    
I tried to do the same solution on the post's first code, but it still yields a compile error of Object is possibly 'undefined'
	.
    .
    .
    
    if (!newHzl[hanzi].pinyin) {
        continue;
    }

    newHzl[hanzi].pinyin = newHzl[hanzi].pinyin.filter(eachPinyin => pinyinEnglish[eachPinyin]);
} // for loop    
The workaround is to introduce a variable so the compiler will not have a hard time inferring the code's control flow:
    .
    .
    .

    const hanziPinyin = newHzl[hanzi].pinyin;
    if (!hanziPinyin) {
        continue;
    }

    newHzl[hanzi].pinyin = hanziPinyin.filter(eachPinyin => pinyinEnglish[eachPinyin]);
} // for loop   
The problem went away

Tuesday, May 18, 2021

JavaScript splitKeep

Helper function for split keep. Reference: https://medium.com/@shemar.gordon32/how-to-split-and-keep-the-delimiter-s-d433fb697c65
String.prototype.splitKeep = function (tokens) {
    const escaped = escapeRegExp(tokens);
    return this.split(new RegExp(`(?=[${escaped}])|(?<=[${escaped}])`, 'g'));
};


// Not built-in yet https://github.com/tc39/proposal-regex-escaping

// Use a good one for the meantime https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript
function escapeRegExp(text) {
    return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
Add this when using TypeScript:
declare global {
    interface String {
        splitKeep(tokens: string): string[];
    }
}
November 14 For browsers that the regex component don't have lookbehind capability yet, use match method:
String.prototype.splitKeep = function (tokens) {
    const tokensEscaped = tokens
        .split('')
        .map((s) => escapeRegExp(s))
        .join('|');

    const wordMatch = `[^${escapeRegExp(tokens)}]+`;
    return this.match(new RegExp(tokensEscaped + '|' + wordMatch, 'g'));
};

Saturday, May 15, 2021

Handling both single click and double click in JavaScript

Live code: https://jsfiddle.net/91zoa2qj/1/

HTML and CSS:
<div id="something">Click or double click me</div>
<hr/>
<div id="good">Click or double click me</div>


<style>
#something, #good {
  background-color: lemonchiffon;
}
</style>

JS:
addGlobalEventListener(
    'click',
    '#something',
    debounceSingleClickOnly(sayHello)
);

addGlobalEventListener(
    'dblclick',
    '#something',
    sayWorld
);

addGlobalEventListener(
    'click',
    '#good',
    debounceSingleClickOnly(sayHello)
);

addGlobalEventListener(
    'dblclick',
    '#good',
    sayWorld
);


    
let counter = 0;
function sayHello({target: {id}}) {
    ++counter;
	console.log(`${counter}. clicked ${id}`);
}


function sayWorld({target: {id}}) {
    ++counter;
	console.log(`${counter}. double-clicked ${id}`);
}


function addGlobalEventListener(type, selector, callback) {
    document.addEventListener(type, (e) => {
        if (e.target.matches(selector)) {
            callback(e);
        }
    });
}


function debounce(func, wait, immediate) {
    let timeout;
    return function () {
        const context = this,
            args = arguments;
        const later = function () {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
}

function debounceSingleClickOnly(func, timeout = 500) {
    function eventHandler(event) {
        const { detail } = event;
        if (detail > 1) {
            return;
        }

        func.apply(this, arguments);
    }

    return debounce(eventHandler, timeout);
}

Thursday, April 29, 2021

Toggling boolean without using global variable

<div id='_form'>
    Form Inputs
</div>

<div id='_preview' style='display: none'>
    Preview
</div>

<button id='_toggle'>
   Toggle
</button>

<script>
window._toggle.onclick = () => previewFormInputs();
// can also do the following instead of the above
/* window._toggle.onclick = previewFormInputs; */

function setElementVisibility(element, visible) {
    element.style.display = visible ? 'inherit' : 'none';
}

let _previewVisible = false;
function previewFormInputs() {
    _previewVisible = !_previewVisible;
    setElementVisibility(window._form, !_previewVisible);
    setElementVisibility(window._preview, _previewVisible);
}
</script>
Live: https://jsfiddle.net/sewa4jh1/1/
The code above can be improved by combining both the action and the state _previewVisible
<div id='_form'>
    Form Inputs
</div>
 
<div id='_preview' style='display: none'>
    Preview
</div>
 
<button id='_toggle'>
   Toggle
</button>
 
<script>
window._toggle.onclick = () => previewFormInputs();
// can also do the following instead of the above
// _stats.onclick = previewFormInputs;
 
function setElementVisibility(element, visible) {
    element.style.display = visible ? 'inherit' : 'none';
}
 
const previewFormInputs = (function () {
    let previewVisible = false;
     
    return function () {
        previewVisible = !previewVisible;
        setElementVisibility(window._form, !previewVisible);
        setElementVisibility(window._preview, previewVisible);
    }   
})();
</script>
Live: https://jsfiddle.net/sewa4jh1/3/

Sunday, April 25, 2021

Do not be driven nuts by multiple nots


Multiple nots. Complex
    firstname != 'Juan' || lastname != 'Cruz'
English-speak: 
    If your firstname is not Juan or your lastname is not Cruz, therefore you are not Juan Cruz

Don't use the above, it has multiple nots. Use this:

Single not, simple. Translates well to English:
    !(firstname == 'Juan' && lastname == 'Cruz')
English-speak (reading from inside out): 
    If you are Juan Cruz, then don't.
English-speak (reading from left to right): 
    If you are not Juan Cruz, then do.
Postgres tuple test:
    (firstname, lastname) != ('Juan', 'Cruz')
Languages without tuple:
    firstname + ' ' + lastname != 'Juan Cruz'

**Schema (PostgreSQL v13)**

    create table person(firstname text, lastname text);
    
    insert into person(firstname, lastname) values
    ('Juan', 'Cruz'),
    ('Juan', 'Buen'),
    ('Michael', 'Cruz'),
    ('Michael', 'Buen');
    

---

**Query #1**

    select
        firstname, lastname,
        
        firstname != 'Juan' or lastname != 'Cruz' as test1,
        not (firstname = 'Juan' and lastname = 'Cruz') as test2,
        
        (firstname, lastname) != ('Juan', 'Cruz') as test3,
        (firstname || ' ' || lastname) != 'Juan Cruz' as test4
        
    from 
    	person;

| firstname | lastname | test1 | test2 | test3 | test4 |
| --------- | -------- | ----- | ----- | ----- | ----- |
| Juan      | Cruz     | false | false | false | false |
| Juan      | Buen     | true  | true  | true  | true  |
| Michael   | Cruz     | true  | true  | true  | true  |
| Michael   | Buen     | true  | true  | true  | true  |

---

[View on DB Fiddle](https://www.db-fiddle.com/f/nxbuszjT4zgmdj4pGaZeuQ/0)


-------------

Multiple nots. Complex
    firstname != 'Juan' && lastname != 'Cruz'

Single not, simple. Translates well to English:
    !(firstname == 'Juan' || lastname == 'Cruz')
English-speak: 
    If your firstname is Juan or lastname is Cruz, then don't.


**Schema (PostgreSQL v13)**

    create table person(firstname text, lastname text);
    
    insert into person(firstname, lastname) values
    ('Juan', 'Cruz'),
    ('Juan', 'Buen'),
    ('Michael', 'Cruz'),
    ('Michael', 'Buen');
    

---

**Query #1**

    select
        firstname, lastname,
        
        firstname != 'Juan' and lastname != 'Cruz' as test1,
        not (firstname = 'Juan' or lastname = 'Cruz') as test2
        
    from 
    	person;

| firstname | lastname | test1 | test2 |
| --------- | -------- | ----- | ----- |
| Juan      | Cruz     | false | false |
| Juan      | Buen     | false | false |
| Michael   | Cruz     | false | false |
| Michael   | Buen     | true  | true  |

---

[View on DB Fiddle](https://www.db-fiddle.com/f/nxbuszjT4zgmdj4pGaZeuQ/1)


Thursday, April 22, 2021

Intl.Segmenter

It would be awesome if Intl.Segmenter is perfect. For the meantime, retokenize the words that should not be together
function tokenizeZH(text) {
    const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
    const segments = segmenter.segment(text);

    const words = [];
    for (const { segment /* , index, isWordLike */ } of segments) {
        words.push(segment);
    }

    return words;
}

console.log(tokenizeZH('我不是太清楚'));

Live: https://jsfiddle.net/rgqen1zc/
Output:
["我不是", "太", "清楚"]

我不是 should be 我 不是

Monday, April 19, 2021

Unchecked runtime.lastError: The message port closed before a response was received.

Even if the listener's async code returns true to indicate it is running an async code, the code will still have the error "Unchecked runtime.lastError: The message port closed before a response was received" if the async code is wrongly placed.
The following code produces the error mentioned due to misplaced async
chrome.runtime.onMessage.addListener(async (message /* , sender, sendResponse */) => {
    if (message.action === UPDATE_PAGE) {
        await applyStyleFromSettings();
    }
    // https://stackoverflow.com/questions/53024819/chrome-extension-sendresponse-not-waiting-for-async-function    
    return true;
});
Returning true is not enough, to fully fix the problem, we must place the async code on the code body itself, not on the callback's declaration. Remove the async declaration from the listener, move it to the code's body instead
chrome.runtime.onMessage.addListener((message /*, sender, sendResponse */) => {
    (async () => {
        if (message.action === UPDATE_PAGE) {
            await applyStyleFromSettings();
        }
    })();
    // https://stackoverflow.com/questions/53024819/chrome-extension-sendresponse-not-waiting-for-async-function            
    return true;
});

Saturday, April 17, 2021

TypeScript, array map can't be type-checked

Given this interface:
	interface IHskLevel {
       hanzi: string;
       hsk: number;
    }
This code does not produce compile error (it should):
function* generateHskLevel(): Iterable<IHskLevel> {
    yield* json.map(
        ({ hanzi, pinyin, HSK: hsk }) => ({ hanzi, pinyin, hsk })
    );
}
This produces compile error (it should):
function* generateHskLevel(): Iterable<IHskLevel> {
    for (const { hanzi, pinyin, HSK: hsk } of json) {
        yield { hanzi, pinyin, hsk };
    }
}
Error:
error: TS2322 [ERROR]: Type '{ hanzi: string; pinyin: string; hsk: number; }' is not assignable to type 'IHskLevel'.
  Object literal may only specify known properties, and 'pinyin' does not exist in type 'IHskLevel'.
            pinyin,

Wednesday, April 14, 2021

Uncaught (in promise) The message port closed before a response was received

// eslint-disable-next-line no-undef
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.dataNeeded === HANZI) {
        (async () => {                                       
            const hzl = await loadHanziListFile();                    
            sendResponse({data: hzl});
        })();
    
        // Need to return true if we are using async code in addListener.
        // Without this line..
        return true;
        // ..we will receive the error: 
        // Uncaught (in promise) The message port closed before a response was received
    }
});

Sunday, April 11, 2021

Service worker registration failed. Cannot read property 'onClicked' of undefined

Given this background.js:
// eslint-disable-next-line no-undef
chrome.action.onClicked.addListener((tab) => {
    console.log("working");

    // eslint-disable-next-line no-undef
    chrome.tabs.sendMessage(
        // tabs[0].id,
        tab.id,
        { action: "CHANGE_COLOR" },
        // eslint-disable-next-line no-unused-vars
        function (response) {}
    );

});

I'm using manifest version 3:
{
    "name": "Chinese word separator",
    "description": "Put spaces between words",
    "version": "1.0",
    "manifest_version": 3,    
    "background" : {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "css": [],
            "js": ["contentScript.js"]
        }
    ]  
}


And I received this error:

To fix the error, add the action property even if it is empty:

{
    "name": "Chinese word separator",
    "description": "Put spaces between words",
    "version": "1.0",
    "manifest_version": 3,    
    "action": {},
    "background" : {
        "service_worker": "background.js"
    },
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "css": [],
            "js": ["contentScript.js"]
        }
    ]  
}
It is advisable to wrap the background.js via a wrapper so you'll get better error message, so if there's no action property in manifest.json..
{
    "name": "Chinese word separator",
    "description": "Put spaces between words",
    "version": "1.0",
    "manifest_version": 3,    
    "background" : {
        "service_worker": "background-wrapper.js"
    },
    "content_scripts": [
        {
            "matches": ["<all_urls>"],
            "css": [],
            "js": ["contentScript.js"]
        }
    ]  
}
..and you wrap the background via wrapper (e.g., background-wrapper.js)..
try {
    // eslint-disable-next-line no-undef
    importScripts("./background.js");
} catch (e) {
    console.error(e);
}
..you will receive a more descriptive error instead:
background-wrapper.js:5 TypeError: Cannot read property 'onClicked' of undefined
    at background.js:1
    at background-wrapper.js:3
(anonymous) @ background-wrapper.js:5
To fix the error, add action on manifest.json

Tuesday, March 9, 2021

Centering horizontally and vertically

<div id="parent">
    <div id="child">hello world</div>
</div>
Centering with margin, it has to be done at child level:
#parent {
    width: 50vw;
    height: 50vh;
    background-color: gray;
    display: flex;
}
#child {
    margin: auto; 
}
Try it at: https://jsfiddle.net/xd7uwgn8/

It's better if it is done at the parent level, the parent should be able to control the child elements:
#parent {
    width: 50vw;
    height: 50vh;
    background-color: gray;
    
    display: flex;
    justify-content: center;
    align-items: center;
}
#child {
}
Try it at: https://jsfiddle.net/xd7uwgn8/2/

Another good thing if it is done at parent level, creating unnecessary div+margin:auto for the content could be avoided:
<div id="parent">
    hello world
</div>
Try it at: https://jsfiddle.net/xd7uwgn8/3/

Tuesday, February 16, 2021

Dvorak navigations - there's no place like home keys

Dvorak keyboard layout is well-known for letting users type commonly used words using just the home keys. And there is no better way to improve that further than making the users use the letters themselves for moving the cursor around.
With the karabiner configuration below, users no longer need to lift their right arm to reach the arrow keys at the rightmost side of their keyboard.
The Karabiner-Element configuration below enable users to use the home keys for cursor navigation for Dvorak users

For best experience, map caps lock to control key, map left control key to esc key.

Not all apps honor the unix-y way of moving the cursor, the Karabiner-Element configuration below will enable it on almost all apps (except Microsoft Office). Microsoft Office apps still interprets Control+B as bold, Control+F as find

I noticed that on online code editors like codepen.io, though they recognize control+F and control+B as right and left arrow respectively, they does not recognize option+control+F as one word to the right, and option+control+B as one word to the left. Worse yet, option+control+F triggers full screen on codesandbox.io, the configuration below solves this problem too.

Up = control+P
Down = control+N
Left = control+B
Right = control+F

option + Right (One word to the right) = option + control+F
option + Left (One word to the left) = option + control+B

delete (hack/delete one character to the left) = control+H
fn+delete (delete one character to the right) = control+D

option + delete (hack/delete one word to the left) = option + control+H
option + fn+delete (delete one word to the right) = option + control+D


command/shift + delete (from the cursor, delete up to end of line) = control+alt+K (did not use control+K, will interfere with terminal's control+K)
no available shortcut key (delete the whole line) = control+alt+U (did not use control+U, will interfere with terminal's control+U)

{
    "description": "Dvorak navigations - no place like home keys",
    "manipulators": [
        {
            "from": {
                "key_code": "v",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "end",
                    "modifiers" : [
                        "left_shift"
                    ]

                },
                {
                    "key_code": "delete_or_backspace"
                }
            ],
            "type": "basic"                                
        },
        {
            "from": {
                "key_code": "f",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"                                            
                    ]
                }
            },
            "to": [
                {
                    "key_code": "home"
                },                                    
                {
                    "key_code": "end",
                    "modifiers" : [
                        "left_shift"
                    ]

                },
                {
                    "key_code": "delete_or_backspace"
                }
            ],
            "type": "basic"                                
        },                            
        {
            "from": {
                "key_code": "j",
                "modifiers": {
                    "mandatory": [
                        "left_control"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "delete_or_backspace"
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "j",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "delete_or_backspace",
                    "modifiers": [
                        "left_option"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "h",
                "modifiers": {
                    "mandatory": [
                        "left_control"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "delete_forward"
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "h",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "delete_forward",
                    "modifiers": [
                        "left_option"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "l",
                "modifiers": {
                    "mandatory": [
                        "left_control"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "down_arrow"
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "l",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_shift"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "down_arrow",
                    "modifiers": [
                        "left_shift"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "r",
                "modifiers": {
                    "mandatory": [
                        "left_control"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "up_arrow"
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "r",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_shift"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "up_arrow",
                    "modifiers": [
                        "left_shift"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "y",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "right_arrow",
                    "modifiers": [
                        "left_option"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "y",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option",
                        "left_shift"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "right_arrow",
                    "modifiers": [
                        "left_option",
                        "left_shift"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "n",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "left_arrow",
                    "modifiers": [
                        "left_option"
                    ]
                }
            ],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "n",
                "modifiers": {
                    "mandatory": [
                        "left_control",
                        "left_option",
                        "left_shift"
                    ]
                }
            },
            "to": [
                {
                    "key_code": "left_arrow",
                    "modifiers": [
                        "left_option",
                        "left_shift"
                    ]
                }
            ],
            "type": "basic"
        }
    ]
},

Friday, February 5, 2021

create react app + typescript script


% cat ~/.scripts/cra.sh 
#!/bin/zsh

if [ $# -eq 0 ] ; then
	echo "Pass the name of project"
	exit 1
fi

NAME="$1"

yarn dlx create-react-app $NAME --template typescript 

cd $NAME
yarn dlx @yarnpkg/pnpify --sdk vscode

yarn add react-refresh eslint-config-react-app

TO_DECLARE="declare module '@testing-library/react';"
printf $TO_DECLARE >> src/react-app-env.d.ts

code-insiders .