From 380def98056cdb2e1723be5db7c450f35d9ba115 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:27:57 +0100 Subject: [PATCH] Task: Use different approach to build react-ui-components: build each file separately the advantage is we dont have so many empty chunks and less files to publish. Also previously with importing from the unstyled compontents would still include css as the chunking wasnt optimal. --- cssModules.js | 8 +- packages/react-ui-components/esbuild.js | 202 ++++++++++-------- packages/react-ui-components/example/test.jsx | 10 +- 3 files changed, 120 insertions(+), 100 deletions(-) diff --git a/cssModules.js b/cssModules.js index 8ac6b74ceb..7c98168ddc 100644 --- a/cssModules.js +++ b/cssModules.js @@ -1,7 +1,7 @@ const { transform } = require('lightningcss'); const { createHash } = require('crypto'); const fs = require("fs/promises"); -const { dirname } = require("path"); +const { dirname, join } = require("path"); /** * A generic cssModules plugin for esbuild based on lightningcss @@ -55,7 +55,9 @@ const cssModules = (options) => { sourceMap: true, targets: options.targets, drafts: options.drafts, - visitor: options.visitor + visitor: options.visitor, + // this way the correct relative path for the source map will be generated ;) + projectRoot: join(initialOptions.absWorkingDir || process.cwd(), initialOptions.outdir) }); if (!exports) { @@ -64,7 +66,7 @@ const cssModules = (options) => { const id = "css-modules:\/\/" + createHash("sha256").update(path).digest('base64url') + '.css' - const finalcode = code.toString("utf8") + `/*# sourceMappingURL=data:text/plain;base64,${map.toString("base64")} */`; + const finalcode = code.toString("utf8") + `/*# sourceMappingURL=data:application/json;base64,${map.toString("base64")} */`; transpiledCssModulesMap.set( id, diff --git a/packages/react-ui-components/esbuild.js b/packages/react-ui-components/esbuild.js index 5613a5c010..a733de0997 100644 --- a/packages/react-ui-components/esbuild.js +++ b/packages/react-ui-components/esbuild.js @@ -3,106 +3,124 @@ const { compileWithCssVariables } = require('../../cssVariables') const nodePath = require("path") const { writeFile, mkdir } = require("fs/promises") -const packageJson = require("./package.json"); const { cssModules } = require('../../cssModules'); const { build } = require('esbuild'); -build({ - entryPoints: [ - "./src/index.ts", - "./src/unstyled.ts", - "./src/identifiers.ts", +const { readdirSync } = require("fs") - // we use each component as entry point to not bundle all code directly into the index.js - // so we have proper code splitting when importing only a single component from this lib - "./src/enhanceWithClickOutside", - "./src/Badge", - "./src/Bar", - "./src/Button", - "./src/ButtonGroup", - "./src/CheckBox", - "./src/DateInput", - "./src/Dialog", - "./src/DropDown", - "./src/Frame", - "./src/Headline", - "./src/Icon", - "./src/IconButton", - "./src/IconButtonDropDown", - "./src/Label", - "./src/Logo", - "./src/SelectBox", - "./src/SideBar", - "./src/Tabs", - "./src/TextArea", - "./src/TextInput", - "./src/ToggablePanel", - "./src/Tooltip", - "./src/Tree", - "./src/MultiSelectBox", - "./src/MultiSelectBox_ListPreviewSortable", - "./src/SelectBox_Option_SingleLine", - "./src/SelectBox_Option_MultiLineWithThumbnail" - ], - external: Object.keys({...packageJson.dependencies, ...packageJson.peerDependencies}), - outdir: "dist", - sourcemap: "linked", - logLevel: 'info', - target: 'es2020', - assetNames: './_css/[hash]', - chunkNames: './_chunk/[hash]', - color: true, - bundle: true, - splitting: true, - format: "esm", - loader: { - '.js': 'tsx', - '.svg': 'dataurl', - '.css': 'copy' - }, - metafile: true, - write: false, // we dont write directly see `.then()` below - plugins: [ - cssModules( - { - includeFilter: /\.css$/, - visitor: compileWithCssVariables(), - targets: { - chrome: 80 // aligns somewhat to es2020 - }, - drafts: { - nesting: true - }, - cssModulesPattern: `neos-[hash]_[local]` - } - ) - ] -}).then((result) => { - if (result.errors.length) { - return; +/** + * @param {String} dir + * @returns {Generator} + */ +function *walkSync(dir) { + const files = readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + if (file.isDirectory()) { + yield* walkSync(nodePath.join(dir, file.name)); + } else { + yield nodePath.join(dir, file.name); + } } +} + +async function main() { - // check for incorrect build or if we have accidentally bundled dependencies what we dont want at all ;) - for (const path of Object.keys(result.metafile.inputs)) { - if (path.startsWith("src/") || path.startsWith("css-modules:")) { - continue; + /** + * we select all ts,js files that are not tests + * this logic should always align to the include and exclude patterns in `tsconfig.esmtypes.json` + */ + const entryPoints = [...walkSync(nodePath.join(__dirname, "src"))].filter((file) => { + if (/(\.spec\.[^/]+|\.story\.jsx?|\.d\.ts)$/.test(file)) { + return false; } - throw new Error(`File ${path} doesnt belong to the currently bundled package, yet is not listed as dependeny.`) - } + if (/(\.tsx?|\.jsx?)$/.test(file)) { + return true; + } + return false; + }) - // we regex replace unused chunk imports in the js files, - // this fixes the suboptimal output - // see issue https://github.com/evanw/esbuild/issues/2922 + console.time("Build") - const unusedImportsRegex = /^import ".*_chunk\/[^;]+;$/gm; - result.outputFiles.forEach(async (file) => { - await mkdir(nodePath.dirname(file.path), { recursive: true}) + const buildResultsPromise = entryPoints.map((entryPoint) => build({ + entryPoints: [entryPoint], + outbase: "src", + outdir: "dist", + sourcemap: "linked", + bundle: true, + logLevel: 'silent', + target: 'es2020', + assetNames: './_css/[hash]', + color: true, + format: "esm", + write: false, + metafile: true, + loader: { + '.js': 'tsx', + '.svg': 'dataurl', + '.css': 'copy' + }, + plugins: [ + { + name: "bundel-only-ressources-for-each-entry", + setup: ({onResolve}) => { + onResolve({filter: /.*/}, ({path}) => { + if (path.endsWith(".css") || path.endsWith(".svg")) { + return; + } - if (file.path.endsWith(".js") && !file.path.endsWith(".map.js")) { - const replacedUnusedImports = file.text.replace(unusedImportsRegex, ""); - writeFile(file.path, replacedUnusedImports, { encoding: "utf8" }) - } else { - writeFile(file.path, file.contents) + if (path === entryPoint) { + return; + } + + return { + external: true + } + }) + } + }, + cssModules( + { + includeFilter: /\.css$/, + visitor: compileWithCssVariables(), + targets: { + chrome: 80 // aligns somewhat to es2020 + }, + drafts: { + nesting: true + }, + cssModulesPattern: `neos-[hash]_[local]` + } + ) + ] + })) + + const buildResults = await Promise.all(buildResultsPromise) + const writtenFiles = new Set(); + for (const buildResult of buildResults) { + + // dont remove this check + // when building the dist we rely that our source code uses no file extensions when referencing relative js sources + // it ensures our dist build still works eventhough we export `.js` instead of `.ts` (currently there is no import rewrite) + // this check acts as a safe guard to test against malformed imports + // in case you want to import a library with specified extension, just exclude the path from the check below, as this is ok ;) + for (const inputImports of Object.values(buildResult.metafile.inputs).flatMap(input => input.imports)) { + if (inputImports.path.match(/\.(jsx?|tsx?)$/)) { + throw new Error(`NeosBuildCheck: Import ${inputImports.path} probably malformed as it ends with a javascript extension.`) + } } - }) -}) + + for (const file of buildResult.outputFiles) { + if (writtenFiles.has(file.path)) { + continue; + } + writtenFiles.add(file.path); + await mkdir(nodePath.dirname(file.path), { recursive: true }) + writeFile(file.path, file.contents); + } + } + + console.timeEnd("Build") + console.log(`Wrote ${writtenFiles.size} files.`); +} + +main(); diff --git a/packages/react-ui-components/example/test.jsx b/packages/react-ui-components/example/test.jsx index 3f1853aa72..ab1e685b7e 100644 --- a/packages/react-ui-components/example/test.jsx +++ b/packages/react-ui-components/example/test.jsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "react-dom"; -import { IconButton, Icon, Button, Headline, Label, ToggablePanel } from "../dist/index" +import { IconButton, Icon, Button, Headline, Logo, ToggablePanel } from "../dist/index" import './font.css'; // The components @@ -22,19 +22,19 @@ const App = () => ( gap: "2rem", width: "100px" }}> + Hello - - + Header - Contents + Marc Henry war hier ^^ - + )