nathanjhood / ts-esbuild-react

A React starter project template, powered by esbuild's Typescript support, with hot-reloading dev server.
https://nh-pages.stoneydsp.com/ts-esbuild-react/
Other
0 stars 0 forks source link

"eject" command: de-ejecting and dependency injection #14

Open nathanjhood opened 3 weeks ago

nathanjhood commented 3 weeks ago

Issue

Current package.json:

{
"scripts": {
    "start": "tsx ./scripts/start.ts",
    "test": "tsx ./scripts/test.ts",
    "build": "tsx ./scripts/build.ts",
    "type-check": "tsc --noEmit",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --check ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./prettier.config.mjs",
    "format:fix": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./prettier.config.mjs"
  },
  "dependencies": {
    "autoprefixer": "10.4.20",
    "bfj": "8.0.0",
    "browserslist": "4.23.3",
    "dotenv": "16.4.5",
    "dotenv-expand": "^11.0.6",
    "esbuild": "^0.23.1",
    "esbuild-plugin-clean": "^1.0.1",
    "esbuild-plugin-copy": "^2.1.1",
    "esbuild-plugin-tsc": "^0.4.0",
    "fs-extra": "11.2.0",
    "postcss": "^8.4.41",
    "react": "18.3.1",
    "react-app-polyfill": "^3.0.0",
    "react-dev-utils": "^12.0.0",
    "react-dom": "^18.3.1",
    "react-refresh": "0.14.2",
    "resolve": "1.22.8",
    "tailwindcss": "^3.4.10",
    "semver": "7.6.3",
    "tsx": "^4.17.0",
    "web-vitals": "^4.2.3"
  },
  "devDependencies": {
    "@eslint/js": "^9.9.0",
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^13.0.0",
    "@testing-library/user-event": "^13.2.1",
    "@tsconfig/node20": "^20.1.4",
    "@tsconfig/recommended": "^1.0.7",
    "@tsconfig/strictest": "^2.0.5",
    "@types/jest": "^27.0.1",
    "@types/node": "^22.4.0",
    "@types/react": "^18.3.3",
    "@types/react-dev-utils": "^9.0.15",
    "@types/react-dom": "^18.3.0",
    "eslint": "^9.9.0",
    "eslint-config-prettier": "^9.1.0",
    "jest": "29.7.0",
    "jest-resolve": "29.7.0",
    "jest-watch-typeahead": "2.2.2",
    "prettier": "^3.3.3",
    "tslib": "^2.6.3",
    "typescript": "^5.5.4",
    "typescript-eslint": "^8.1.0"
  },
}

Solution

Ideal package.json - resembles a react app that has not been ejected. In our case, we can nicely pull the so-say "react-scripts" package from a directory nested within the project, like a monorepo:

{
  "scripts": {
    "start": "ts-esbuild-react-scripts start",
    "test": "ts-esbuild-react-scripts  test.",
    "build": "ts-esbuild-react-scripts  build",
    "eject": "ts-esbuild-react-scripts  eject",
    "type-check": "tsc --noEmit",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --check ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./prettier.config.mjs",
    "format:fix": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./prettier.config.mjs"
  },
  "dependencies": {
    "autoprefixer": "10.4.20",
    "postcss": "^8.4.41",
    "tailwindcss": "^3.4.10",
    "ts-esbuild-react-scripts": "./packages/ts-esbuild-react-scripts"
  }
}

With the above in place, the idea would be that ts-esbuild-react-scripts - or something it depends on - carries with it all the npm dependencies which our app needs, so that we just carry ts-esbuild-react-scripts, and any additional processors like the CSS tools above.

This will be particularly nice with regards to eslint/prettier (which may also be wrapped in their own dependency/config provider package, or we can use the CRA one).

This also has some added benefits, such as keeping some false-positive errors out of the IDE's intellisense path, where the scripts dir can be safely removed, since ts-esbuild-react-scripts will be carrying them internally, instead of them living at the project root.

Of course, all of the above reflects the changes made when one "ejects" a React app.

And thus, I've done the work required and identified the needs of the app structure enough that I probably could add the "eject" script and functionality in due course.

nathanjhood commented 3 weeks ago

found it...

const escapeStringRegexp = require('escape-string-regexp');

class InterpolateHtmlPlugin {
  constructor(htmlWebpackPlugin, replacements) {
    this.htmlWebpackPlugin = htmlWebpackPlugin;
    this.replacements = replacements;
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('InterpolateHtmlPlugin', compilation => {
      this.htmlWebpackPlugin
        .getHooks(compilation)
        .afterTemplateExecution.tap('InterpolateHtmlPlugin', data => {
          // Run HTML through a series of user-specified string replacements.
          Object.keys(this.replacements).forEach(key => {
            const value = this.replacements[key];
            data.html = data.html.replace(
              new RegExp('%' + escapeStringRegexp(key) + '%', 'g'),
              value
            );
          });
        });
    });
  }
}