Exploring Modern FE Development Environments by Building a Coding Playground Part 2

2021. 2. 22.

Exploring Modern FE Development Environments by Building a Coding Playground Part 2

(Updated 2022-03-04: Configuration code related to webpack-dev-server, babel, and jest has been modified.)

In Part 1, we built a basic JavaScript development environment with Lerna. While the basic setup might be sufficient for some, it usually isn't. Now, we'll add two more development environments: TypeScript and React.

First, we'll add the TypeScript environment. Before that, there's a task we need to do with Lerna. Since the TypeScript environment will ultimately be built on top of the JavaScript environment we've established, we'll make it possible to share dependency modules and extend the configuration files from the basic JavaScript environment. Then, we'll build the React development environment on top of the TypeScript environment. If you want to add only React on top of the basic JavaScript environment without TypeScript, you can skip the TypeScript-related steps.

Adding the pgts package

Now, just like we did in Part 1, we use Lerna's create command to add the TypeScript environment package, pgts.

 npx lerna create pgts

After completing the package setup, the packages/pgts directory should be created. And, of course, Lerna will have created some directories. As we did at the beginning of Part 1, delete the __test__ directory and rename the lib directory to src. Also, rename the src/pgts.js file to src/index.ts. We start with the same environment as pgjs. Copy the contents of packages/pgjs/package.json and overwrite pgts's package.json. Then, just change the name to pgts. It's good practice to modify the main property and directories as well, but they aren't crucial for our playground. If you're bothered by it, feel free to change the other properties too. Actually, you could just copy the contents of packages/pgjs directly into packages/pgts and only modify package.json. There's no need to use Lerna's create. We'll do it that way next time.

Hoisting devDependencies to the root

We've added the pgts package, but it's still empty except for package.json. As mentioned earlier, the dependency modules of pgjs will be shared with pgts as they are common modules. Lerna makes this possible. Lerna provides various features for managing multiple projects in a monorepo. The task we'll perform now is to move the dependency modules to the root so they can be accessed from there. This can be easily done with Lerna's link command.

npx lerna link convert

When the link command runs, the devDependencies previously installed in node_modules are moved to the root, and the contents of package.json in each package and the root are modified. Check it out. Now, the devDependencies modules of pgjs and pgts are shared via the project root.

Conversely, devDependencies that are not shared commonly can be installed as dependencies to prevent them from being moved. Since we're creating a playground and have no intention of deploying, it doesn't matter. We'll just install everything as devDependencies so all dependencies are shared.

Since all dependencies have been moved to the root by this process, the package-lock.json files in the packages are no longer needed, so delete them.

Reducing configuration file duplication

Common modules can be shared from the root, but the pgts environment still has no configuration files, so it can't do anything yet. Now we'll create the configuration files. Since pgjs and pgts use similar environments, their configuration files will likely have a lot of overlapping content. We'll try to reduce duplication as much as possible while setting things up.

Starting with ESLint configuration

ESLint configurations defined in .eslintrc.js can be extended from other files using the extends option. In Part 1, we configured ESLint in packages/pgjs/.eslintrc.js. Now, we'll move this file to the packages directory and make it usable by both pgjs and pgts. First, move the file. Then, create new .eslintrc.js files in packages/pgjs and packages/pgts respectively, and enter the following:

module.exports = {
  extends: '../.eslintrc.js', // Adjusted path
};

Actually, the pgjs package will use the shared .eslintrc.js moved to the packages directory as is, so there's no need to create packages/pgjs/.eslintrc.js separately. ESLint searches parent directories for configuration files if one isn't found, making it unnecessary. However, we created it for the future, in case specific rules need to be set separately. Now, based on the basic configuration, pgts and pgjs can add their own specific ESLint settings.

Next is Babel configuration

Babel configuration files are similar. Just like ESLint, you can extend configuration files using the extends option. The usage is also identical. Those with quick hands might already be doing it. Copy the packages/pgjs/.babelrc file to the packages directory. Then, create new .babelrc files in packages/pgjs and packages/pgts respectively, and enter the following:

{
  "extends": "../.babelrc"
}

For now, both pgts and pgjs use the same environment, so the Babel configuration remains the same. Later, TypeScript settings will be added to the pgts Babel configuration.

And finally, Webpack

Webpack doesn't provide an extension option like extends. However, since the webpack.config.js file is just a JavaScript file that needs to return a configuration object, extending it is easy. I created a file named packages/webpack.config.base.js and defined a function within it that returns the basic configuration. Each package will then import and use this function. Let's try it.

First, create the packages/webpack.config.base.js file, then copy and paste the contents of packages/pgjs/webpack.config.js into it. This content will effectively become the base Webpack environment. However, since __dirname will differ when imported externally, we'll add a parameter to receive the path and use it.

// ..
// Add dirname parameter, default to __dirname for standalone use if needed
module.exports = (env, argv, dirname) => { 
 // ..
 entry: [], // Line 7: Modify to an empty array
 // .. 
 path: path.resolve(dirname, 'dist'),   // Line 10: Modify path resolution
 // .. 
     '@src': path.resolve(dirname, 'src/'),  // Line 26: Modify alias resolution
// ..
}

The entry point will differ for each package, so we make it an empty array in the base configuration. It will be added by the extending configuration.

Now that we've done this, let's create the Webpack configurations in packages/pgjs and packages/pgts that extend the base configuration. For pgjs, we'll modify the existing webpack.config.js file, and for pgts, we'll need to add a webpack.config.js file.

const baseConfig = require('../webpack.config.base');

module.exports = (env, argv) => {
  // Pass __dirname to the base config function
  let config = baseConfig(env, argv, __dirname); 

  // In `packages/pgts`, use `index.ts`
  config.entry.push('./src/index.js'); 

  return config;
};

They are almost identical, but in pgts, the entry point file extension should be ts, the TypeScript extension. Refer to the comment and make the change.

Shall we start the server to test? Before testing, we'll add an npm script to start the server. Because dependencies were linked with Lerna, we can't run Webpack directly with npx. Add the script to package.json in both packages/pgts and packages/pgjs.

"scripts": {
   "serve": "webpack serve --mode development",
   "test": "jest"
 },

That's all for reducing configuration file duplication. Actually, Jest is left, but like Webpack, Jest doesn't provide an option for configuration extension. However, similar to Webpack, the configuration file is just a plain JavaScript object, so it can be easily extended using the spread operator. Those who wish can do this separately. I'll pass.

Creating a TypeScript (typescript) development environment

So far, there's no difference between the pgts and pgjs packages. They are completely identical. Now, let's enable the pgts package to use TypeScript. We'll perform three tasks:

  1. Configure Babel to understand TypeScript.
  2. Configure tsconfig.json.
  3. Configure ESLint to understand TypeScript.
  4. Configure Jest to understand TypeScript.

Babel for TypeScript

Let's start with the first task: making Babel understand TypeScript.

Configuring Babel is also very easy. Navigate to the packages/pgts directory and install the Babel TypeScript preset. This allows you to install the necessary plugins for using TypeScript with Babel all at once. After lerna link convert, some local package dependencies need to be installed using npm instead of lerna add to install correctly. The exact reason isn't clear, but it seems to be an issue arising from shared node_modules. I'll look into this when I have time. (If anyone knows, please let me know...). Since all local dependencies will eventually be moved to the root using link, this is a temporary issue.

npm i -D @babel/preset-typescript

And we need to add it to the Babel configuration, right? Add the preset to the packages/pgts/.babelrc file.

{
  "extends": "../.babelrc",
  "presets": [
    "@babel/preset-typescript"
  ]
}

Now Babel can transpile TypeScript. Let's check. Modify packages/pgts/src/index.ts as follows:

export function tsTest(a: number, b: number): number {
  return a + b;
}

console.log(tsTest(1, 2));

Then start the server with npm run serve. If the number 3 is printed in the console, it's successful. However, if the Babel configuration is incorrect, an error will occur when starting the server, so you'll know immediately. We add export beforehand to test it later with Jest.

tsconfig.json

Although we can now use TypeScript with Babel, it's still not enough. To fully utilize TypeScript's features, we need to create a tsconfig.json file and configure TypeScript settings. Currently, creating just one packages/pgts/tsconfig.json would suffice, but since we plan to use TypeScript with React later, we'll create a default tsconfig.json and extend it.

Add the packages/tsconfig.json file (Note: the original Korean text mentioned pgtsconfig.json, but the content suggests a base config in the packages root).

{
  "compilerOptions": {
    "noEmit": true, // Do not emit output files (Babel handles transpilation)
    "target": "ESNext", // Target latest ECMAScript features
    "module": "CommonJS", // Module system for Node.js compatibility (Jest)
    "strict": true, // Enable all strict type-checking options
    "importHelpers": true, // Import tslib helpers
    "moduleResolution": "node", // Module resolution strategy
    "experimentalDecorators": true, // Enable experimental decorators
    "esModuleInterop": true, // Allows default imports from CommonJS modules
    "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
    "noImplicitAny": false, // Allow variables with an implicitly 'any' type (can be set to true for stricter checking)
    "sourceMap": true, // Generate source maps
    "lib": ["esnext", "dom", "dom.iterable"], // Standard library files to include
    "types": ["node", "jest"], // Type definition files to include
    "downlevelIteration": true // Provide full support for iterables in 'for-of', spread, and destructuring when targeting ES5 or ES3
  },
  "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"] // Exclude node_modules and test files from compilation check
}

This configuration should probably work as is without special modifications. If you encounter situations requiring changes, you can adjust it slightly according to the circumstances.

Then, create the configuration file packages/pgts/tsconfig.json that extends packages/tsconfig.json.

{
  "extends": "../tsconfig.json", // Extend the base config
  "compilerOptions": {
    "baseUrl": "./", // Base directory for module resolution
    "paths": {
      "@src/*": [ // Path mapping for cleaner imports
        "src/*"
      ]
    }
  },
  "include": [ // Files to include in the TypeScript program
    "jest.config.js",
    ".eslintrc.js",
    "webpack.config.js",
    "src/**/*.js", // Include JavaScript files in src
    "src/**/*.ts", // Include TypeScript files in src
    "src/**/*.tsx" // Include TSX files (for React later)
  ]
}

Here too, we used the extends option to extend the configuration.

ESLint for TypeScript

ESLint also needs to be configured appropriately for TypeScript to perform static analysis correctly. Only then can this friend nag us properly. The setup is simple. First, install the necessary dependencies.

npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

Install the parser and plugin for TypeScript, then immediately modify the packages/pgts/.eslintrc.js file.

module.exports = {
  parser: '@typescript-eslint/parser', // Specify the TypeScript parser
  extends: [
    '../.eslintrc.js', // Extend the base ESLint config
    'plugin:@typescript-eslint/recommended' // Use recommended TypeScript rules
  ],
  plugins: ['@typescript-eslint'], // Enable the TypeScript plugin
   parserOptions: {
    ecmaFeatures: {
      impliedStrict: true, // Enable global strict mode
    },
    project: './tsconfig.json', // Point ESLint to the tsconfig file for type-aware linting
    tsconfigRootDir: __dirname, // Specify the root directory for tsconfig.json
  },
};

It should probably work without major issues. If you want to verify, you can make some minor mischievous changes (?) to the index.ts file and check if ESLint gives TypeScript warnings.

Jest for TypeScript

This time, let's configure Jest so it can also test TypeScript code. It follows the same pattern: Install necessary dependencies! Then configure!

Install! In packages/pgts

npm i -D ts-jest

Configure! packages/pgts/jest.config.js

module.exports = {
  preset: 'ts-jest/presets/js-with-babel', // Use ts-jest preset that works with Babel
  testEnvironment: 'jsdom', // Set the test environment to jsdom for browser-like environment
  moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'd.ts'], // File extensions Jest should look for
  moduleNameMapper: {
    '^@src/(.*)$': '<rootDir>/src/$1', // Map @src alias to the src directory
  },
  globals: {
    'ts-jest': {
      babelConfig: true, // Tell ts-jest to use Babel configuration
    },
  },
  watchPathIgnorePatterns: ['/node_modules/'], // Ignore node_modules when watching files
};

This configuration uses the TypeScript preset for Jest, specifically the preset that utilizes Babel.

Test Code! packages/pgts/src/index.test.ts

import { tsTest } from './index';

describe('tsTest', () => {
  it('needs tests', () => {
    expect(tsTest(1, 2)).toEqual(3);
  });
});

Verify!

> npm test

 PASS  src/index.test.ts
  tsTest
     needs tests (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        ...s
Ran all test suites.

You should probably have completed the tests successfully without any particular issues. However, if you open the index.test.ts file, you'll likely see TypeScript warnings at the describe and it function locations. This happens because TypeScript doesn't know about the Jest APIs. Installing the Jest type definitions will resolve this.

npm i -D @types/jest

Now, the TypeScript environment setup is all finished.

Creating a React (React) development environment

We will build the React environment on top of the TypeScript environment. More accurately, it's a React development environment on top of a TypeScript environment, which is on top of a JavaScript environment. We are building it up step by step. Setting up the React environment is similar to creating pgts from pgjs. We will slightly modify the environments we are using to fit React. First, let's add the package.

Adding the react package

For convenience, this time we won't use lerna create to make the package. We'll just copy it. Create the packages/pgreact directory and copy everything from packages/pgts except node_modules. Don't forget to copy hidden files like .babelrc. After copying, although it's not strictly a problem, change the name in package.json to pgreact. Then, install dependencies with npm i, try starting the server, and run the tests. It should probably work fine without issues, as it's currently identical to pgts.

React installation and Babel configuration

Since we're creating a React development environment, we naturally need to install React. And since we need to use jsx, we'll also configure Babel to recognize React. First, let's install all the necessary dependencies at once.

Run this in packages/pgreact:

npm i react react-dom core-js
npm i -D @babel/preset-react

(Note: react and react-dom are dependencies, core-js might be needed for polyfills depending on Babel setup, and @babel/preset-react is a dev dependency).

React can be used immediately after installation, but using jsx requires additional steps because browsers don't understand jsx. We'll configure Babel to transpile jsx into valid JavaScript code. We've already installed the dependency, so we just need to configure it.

Add @babel/preset-react to the presets array option in .babelrc. This means we'll use the preset that bundles necessary Babel plugins for React.

{
  "extends": "../.babelrc",
  "presets": [
    "@babel/preset-typescript",
    "@babel/preset-react" // Add this
  ]
}

Now, let's try using React. Change the extension of the packages/pgreact/src/index.ts file to tsx and create a simple React component in this file.

import React from 'react';
import ReactDOM from 'react-dom';

const Hello: React.FC = () => {
  return <h1>Hello World</h1>;
};

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<Hello />, container); // Render into a container

Since we changed the entry point file extension, update it in the packages/pgreact/webpack.config.js configuration as well.

// …
// Change ts to tsx
config.entry.push('./src/index.tsx'); 
// …

Now it seems like we can start the server, but not yet. Because we're integrating Babel through Webpack, and we haven't told the Webpack babel-loader to process tsx files yet. Let's modify that part. This setting is in packages/webpack.config.base.js.

//…
module: {
      rules: [
        {
          // Modify this regex to include tsx and jsx
          test: /\.(ts|tsx|js|jsx)$/, 
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
//…

And one more thing, declare that we'll use jsx in packages/pgreact/tsconfig.json.

{
  // …
  "compilerOptions": {
    "baseUrl": "./",
    "paths": { 
        // ... paths should exist here from pgts/tsconfig.json
        "@src/*": [
          "src/*"
        ]
     },
    "jsx": "react-jsx" // Add this (or "react" for older versions)
  },
  // Ensure include covers tsx
  "include": [
    "jest.config.js",
    ".eslintrc.js",
    "webpack.config.js",
    "src/**/*.js",
    "src/**/*.ts",
    "src/**/*.tsx" 
  ]
 //…
}

(Note: Added react-jsx which is common for modern React setups, and ensured paths and include are present).

Not doing this won't cause major problems, but TypeScript keeps warning, which is annoying.

Now, start the server to check. "Hello World" should appear, right? If not, glare back at the steps above.

ESLint for React

Now that the basic environment is set up, let's ask ESLint to monitor our React code as well. It helps find potential mistakes in the code and provides various nags, making ESLint essential for React development too. First, let's install the ESLint plugins.

npm i -D eslint-plugin-react eslint-plugin-react-hooks @types/react @types/react-dom

We installed React type declarations and the React Hooks plugin together. Whatever you say, using Hooks is better. I think it's just a matter of familiarity. Once the plugin installation is complete, add the plugins to the configuration.

packages/pgreact/.eslintrc.js

module.exports = {
  extends: [
   '../.eslintrc.js', // Base config
   'plugin:@typescript-eslint/recommended', // TS rules (should inherit from pgts base ideally)
   'plugin:react/recommended', // Add React recommended rules
   'plugin:react-hooks/recommended', // Add React Hooks rules
  ],
  plugins: [ 
   '@typescript-eslint', // TS plugin (should inherit)
   'react',  // Add react plugin
   'react-hooks'  // Add react-hooks plugin
  ],
  settings: {   // Add settings section
    react: {
     version: 'detect', // Automatically detect React version
    },
  },
  // Ensure parser and parserOptions are correctly set for TSX
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true, // Enable JSX parsing
      impliedStrict: true,
    },
    ecmaVersion: 2021, // Or newer
    sourceType: 'module',
    project: './tsconfig.json',
    tsconfigRootDir: __dirname,
  },
  // Add rules if needed, e.g., disabling prop-types if using TypeScript
  rules: {
    'react/prop-types': 'off' 
  }
};

(Note: Made adjustments to ensure TS/React settings integrate properly and added react/prop-types: off as it's common with TypeScript).

This completes the ESLint setup. Shall we check if it's configured correctly? Let's add code using hooks to the Hello component in index.tsx. If you change the code as below, a react-hooks/exhaustive-deps warning should appear if the setup is correct.

import React, { useState, useEffect } from 'react';
// ... rest of imports

const Hello: React.FC = () => {
  const [a, /*setA*/] = useState(0); // Added state, commented out setter to avoid unused var warning

  useEffect(() => {
    console.log(a);
  }, []); // Warning appears here
  
  return <h1>Hello World</h1>;
};

// ... rest of file (ReactDOM.render)

Just check and revert the changes.

Jest for React

Although Jest can be used for E2E tests, here we only consider unit tests. Since Jest is already installed, we can write test code immediately. However, for testing convenience, we'll add a utility tool. Its name is Testing Library (testing-library). It provides useful functions for writing front-end test code, enabling quick and easy test writing. It supports not only React but also various frameworks like Vue, Angular, and even the DOM. Unless there's a specific reason not to, its use is recommended.

npm i -D @testing-library/react @testing-library/jest-dom

(Note: Added @testing-library/jest-dom for useful matchers).

The Jest configuration already considers React, so there's no need to touch the settings separately. We might need to setup jest-dom matchers globally. Let's add a setup file.

Create packages/pgreact/jest.setup.js:

import '@testing-library/jest-dom';

Modify packages/pgreact/jest.config.js:

module.exports = {
  // ... existing config
  preset: 'ts-jest/presets/js-with-babel',
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'd.ts'],
  moduleNameMapper: {
    '^@src/(.*)$': '<rootDir>/src/$1',
  },
  globals: {
    'ts-jest': {
      babelConfig: true,
    },
  },
  watchPathIgnorePatterns: ['/node_modules/'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], // Add this line
};

Now, let's write a simple test code for the Hello component. Currently, the Hello component is defined in the entry point file, so let's first separate it into its own file.

packages/pgreact/src/hello.tsx

import React from 'react';

const Hello: React.FC = () => {
  return <h1>Hello World</h1>;
};

export default Hello;

The index.tsx file now uses the separated Hello component.

import React from 'react';
import ReactDOM from 'react-dom';
import Hello from './hello'; // Import the component

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(<Hello />, container);

Rename the packages/pgreact/src/index.test.ts file extension to tsx. This way, TypeScript won't warn even if we use jsx. Then, write a simple test code for the Hello component.

packages/pgreact/src/hello.test.tsx (renamed from index.test.ts)

import React from 'react';
import { render, screen } from '@testing-library/react'; // Import screen

import Hello from './hello';

describe('Component test', () => {
  it('renders hello world heading', () => { // More descriptive test name
    render(<Hello />); // Render the component

    // Use screen queries - preferred way
    const headingElement = screen.getByText('Hello World'); 
    
    expect(headingElement).toBeInTheDocument(); // Check if element exists
    expect(headingElement.nodeName).toEqual('H1'); // Check if it's an H1
  });
});

(Note: Renamed file, used screen from testing-library, improved test description and assertion).

Using Testing Library's render function, we render the Hello component into a virtual DOM. Then, using the getByText function (via screen), we find the node in the currently rendered DOM that contains the text "Hello World". Finally, we verify if the found node is indeed an H1.

Since the Hello component is very simple and lacks logic, we wrote this kind of test code. However, in situations without logic that modifies elements, testing the node name is meaningless. Such simple rendering doesn't even need testing, and if you must, use snapshots. Also, checking the element's name is often a meaningless test. Element structure can change, and whether the node structure itself is truly important... blah blah blah... Again, it's turning into a "test sermon," so I'll cut the unnecessary rambling.

Styled Components (styled-component)

There have been many attempts to integrate CSS into JavaScript code or framework components. I've reviewed a few options myself and currently settled on Styled Components. Tools like this can actually be replaced with others at any time. Trying different approaches for each project could be interesting. Anyway, since I've been using Styled Components recently, I'll install it as the final step for the React environment and wrap up.

npm i styled-components
npm i -D @types/styled-components babel-plugin-styled-components

(Note: Added types and babel plugin for better DX/SSR).

Styled Components run under JavaScript syntax, so you can use them immediately after installation. Shall we modify index.tsx? (Actually, let's modify hello.tsx to style the component itself).

Modify packages/pgreact/.babelrc to include the plugin:

{
  "extends": "../.babelrc",
  "presets": [
    "@babel/preset-typescript",
    "@babel/preset-react"
  ],
  "plugins": [ // Add plugins section
    "babel-plugin-styled-components" 
  ]
}

Modify packages/pgreact/src/hello.tsx:

import React from 'react';
import styled from 'styled-components'; // Import styled

// Define the styled component
const Title = styled.h1`
  color: blue;
`;

// Use the styled component instead of a regular h1
const Hello: React.FC = () => {
  return <Title>Hello World</Title>; 
};

export default Hello;

(Note: Modified hello.tsx instead of index.tsx for better component encapsulation).

We added a styled component named Title and applied color using CSS. When first applying Styled Components, the different workflow compared to traditional CSS can be confusing. A broad but simple tip: think of each styled component as a CSS class. This mindset might help a bit when structuring.

lerna link convert once more

Since there are also duplicated dependencies in ts and react, these too should be moved to the root and shared by everyone from the root node_modules.

We did this at the beginning, right? One command is all it takes.

npx lerna link convert

(Note: The original text used lerna convert link, but lerna link convert is the correct command shown earlier).

If you check package.json, you can see that only the dependencies needed exclusively for the React environment remain, and everything else has moved to the root. Now you can save hard drive space even while creating tens of prototype or experimental projects :)

Utilizing the playground

Alright, the environment setup I needed is complete for now. Previously, when watching development-related YouTube videos, if I wanted to try out the example code, I'd open the editor but then get discouraged by the hassle of setting up the necessary environment and just decide to watch. Now, I can simply copy the required environment, give it a new name, and immediately experiment in a comfortable editor setting. For example, if the needed environment is React, I can create a directory like packages/mytest, copy the contents of packages/pgreact straight into it, and start typing the experimental code right away.

I use the same method for contents or prototypes I want to experiment with in an isolated environment during work. Not just for simple tests, but even somewhat serious project prototyping starts here. I create a directory for each project, copy the environment, and then manage it using a separate Git branch. This keeps the main branch clean, maintaining only the environment packages.

Another worthwhile task involves the packages option in lerna.json, which can accept an array of multiple paths. By adding paths like apps/*, for instance, you can separate the packages containing only environment configurations from the projects using those environments for experimentation. This could make management easier. I didn't cover it in this post because it would unnecessarily lengthen the content with path modifications for basic environment settings, but I plan to apply it to the shared repository soon.

One drawback is the need for a feature like "Export" to make a single project independent, but implementing that involves a lot of work. In such situations, you still have to move things manually, step by step.

Another way to utilize

Developers creating open-source projects or libraries often test their work in various supported environments at least once before deployment. For open-source libraries, vanilla JavaScript is a given, and it must also run well in ESM and TypeScript environments with clear typings. If support extends to framework components like React, Vue, and Angular, the number of usage environments to test can become overwhelmingly large. In such cases, pre-configuring each environment in the playground and pulling it out for testing whenever needed makes managing each environment easier and the testing itself much more convenient. I use it this way too. Furthermore, even modules not yet published to NPM can be used as dependencies and tested as if they were published, using Lerna's bootstrap command with local modules. This is also one of Lerna's strengths.

Wrapping up and one missing piece

So, we've reached the end of creating a playground where we can easily add simple projects anytime.

Actually, one stack is missing compared to the initial plan: Storybook. If projects were managed per environment using Storybook, you could add stories whenever needed for independent experiments, right? It also provides a gallery-style UI, making management easy. With Storybook, there would be no need to copy directories when a specific environment is needed. The directory copying method is an unavoidable temporary measure to fill the gap left by Storybook's absence. The playground initially envisioned was completed with Storybook. The reason Storybook was omitted is that it didn't support Webpack 5 at the time. It had to be excluded. Webpack 5 was facing many difficulties due to third-party migration issues. Someday, when Storybook supports Webpack 5, I will write Part 3 to conclude. For now, we have to rely on directory copying.

Anyway, using the playground as a pretext, we've touched upon the essential elements of the modern FE development environment one by one. Depending on the project being developed, many more tools might need to be added, but most will likely be built on top of this environment. They are like sturdy pillars.

There's a possibility of changes or additions next year. Several tools are currently being considered. If the opportunity arises next year, I will introduce them then.

The completed hatchery repository is here.

♥ Support writer ♥
with kakaopay

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

shiren • © 2025Sungho Kim