From 829d4c9d46ad3c46c763ae5d523782de8a2353d3 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Wed, 22 Jun 2022 17:03:56 -0700 Subject: [PATCH] Start work on production builds (#1) Note that this is about reducing the runtime overhead of dev-mode constructs, not (yet) about eliminating them from the build. 1. use import.meta.env.PROD in user code 2. add `pnpm test:prod`, which runs the tests in prod mode 3. Make verify() and verified() noops in production 4. Update rollup.config.js to mirror how vite replaces import.meta.env Note that the reason we have rollup.config.ts is because we need the feature of rollup that allows us to create separate, distinct configs, because we don't want the packages to share chunks with each other. See [this vite issue]. [this vite issue]: https://github.com/vitejs/vite/discussions/1736 --- .build/import-meta.js | 15 +++++ .build/replace.js | 58 +++++++++++++++++++ .eslintrc.json | 11 ++-- .vscode/settings.json | 6 +- LICENSE.md | 47 +++++++++++++++ framework/react/use-resource/src/resource.ts | 6 +- .../use-resource/tests/use-resource.spec.ts | 6 +- package.json | 5 ++ packages/env.d.ts | 5 ++ packages/verify/index.ts | 18 +++--- packages/verify/src/verify.ts | 16 +++++ packages/verify/tests/basic.spec.ts | 4 +- packages/verify/tests/verify.spec.ts | 4 +- pnpm-lock.yaml | 56 ++++++++++++++++++ rollup.config.js | 28 ++++++--- tsconfig.json | 4 +- vite.config.ts | 5 -- 17 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 .build/import-meta.js create mode 100644 .build/replace.js create mode 100644 LICENSE.md create mode 100644 packages/env.d.ts diff --git a/.build/import-meta.js b/.build/import-meta.js new file mode 100644 index 00000000..e4a2ce00 --- /dev/null +++ b/.build/import-meta.js @@ -0,0 +1,15 @@ +import { createReplacePlugin } from "./replace.js"; + +const MODE = process.env.MODE ?? "development"; +const DEV = MODE === "development"; +const PROD = MODE === "production"; + +export default createReplacePlugin( + (id) => /\.(j|t)sx?$/.test(id), + { + "import.meta.env.MODE": process.env.MODE ?? "development", + "import.meta.env.DEV": DEV ? "true" : "false", + "import.meta.env.PROD": PROD ? "true" : "false", + }, + true +); diff --git a/.build/replace.js b/.build/replace.js new file mode 100644 index 00000000..d0b2ebab --- /dev/null +++ b/.build/replace.js @@ -0,0 +1,58 @@ +// originally from: https://github.com/vitejs/vite/blob/51e9c83458e30e3ce70abead14e02a7b353322d9/src/node/build/buildPluginReplace.ts + +// import type { Plugin, TransformResult } from "rollup"; +import MagicString from "magic-string"; + +/** + * @param {(id: string) => boolean} test + * @param {Record} replacements + * @param {boolean} sourcemap + * @returns {import("rollup").Plugin} + */ +export function createReplacePlugin(test, replacements, sourcemap) { + const pattern = new RegExp( + "\\b(" + + Object.keys(replacements) + .map((str) => { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); + }) + .join("|") + + ")\\b", + "g" + ); + + return { + name: "starbeam:replace", + /** + * @param {string} code + * @param {string} id + * @returns {import("rollup").TransformResult} + */ + transform(code, id) { + if (test(id)) { + const s = new MagicString(code); + let hasReplaced = false; + let match; + + while ((match = pattern.exec(code))) { + hasReplaced = true; + const start = match.index; + const end = start + match[0].length; + const replacement = replacements[match[1]]; + s.overwrite(start, end, replacement); + } + + if (!hasReplaced) { + return null; + } + + /** @type { import("rollup").TransformResult} */ + const result = { code: s.toString() }; + if (sourcemap) { + result.map = s.generateMap({ hires: true }); + } + return result; + } + }, + }; +} diff --git a/.eslintrc.json b/.eslintrc.json index da080cc8..850ab56d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,6 @@ { "root": true, - "plugins": [ - "prettier", - "unused-imports", - "simple-import-sort" - ], + "plugins": ["prettier", "unused-imports", "simple-import-sort"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": false, @@ -107,7 +103,10 @@ } }, { - "files": ["framework/react/use-resource/scripts/**/*"], + "files": [ + "framework/react/use-resource/scripts/**/*", + "rollup.config.js" + ], "env": { "node": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index 00728e72..0b63a757 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,8 @@ "*.jsx": "${capture}.js", "*.tsx": "${capture}.ts", "tsconfig.json": "tsconfig.*", - "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, .editorconfig, .eslintrc.json, .gitignore, .npmrc, .quokka, pnpm-workspace.yaml, vite.config.ts" + "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, .pnpm-debug.log, .editorconfig, .eslintrc.json, .gitignore, .npmrc, .quokka, pnpm-workspace.yaml", + "vite.config.ts": "rollup.config.js, .env.*" }, "editor.defaultFormatter": "dbaeumer.vscode-eslint", "vitest.enable": true, @@ -30,5 +31,8 @@ }, "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[dotenv]": { + "editor.defaultFormatter": "foxundermoon.shell-format" } } diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..0dc9418c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,47 @@ +MIT License + +Copyright (c) 20222-present, Yehuda Katz and Starbeam contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Some additional code with the following licenses is included. + +MIT License + +Copyright (c) 2019-present, Yuxi (Evan) You + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/framework/react/use-resource/src/resource.ts b/framework/react/use-resource/src/resource.ts index 5897f082..b25d524a 100644 --- a/framework/react/use-resource/src/resource.ts +++ b/framework/react/use-resource/src/resource.ts @@ -278,16 +278,16 @@ export function useResource(): { options?: LifecycleOptions ) => Resource; } { - return useResource.with(undefined as void); + return useResource.withState(undefined as void); } useResource.create = ( create: CreateResource ): Resource => { - return useResource.with(undefined as void).create(create); + return useResource.withState(undefined as void).create(create); }; -useResource.with = ( +useResource.withState = ( state: A ): { create: ( diff --git a/framework/react/use-resource/tests/use-resource.spec.ts b/framework/react/use-resource/tests/use-resource.spec.ts index d80baebc..bae981ec 100644 --- a/framework/react/use-resource/tests/use-resource.spec.ts +++ b/framework/react/use-resource/tests/use-resource.spec.ts @@ -16,7 +16,7 @@ testModes("useResource", (mode) => { const [count, setCount] = useState(0); const resource = useResource - .with({ count }) + .withState({ count }) .create(({ count }) => TestResource.initial(count)) .update((resource, { count }) => resource.transition("updated", count)) .on({ @@ -74,7 +74,7 @@ testModes("useResource (nested)", (mode) => { const [count, setCount] = useState(0); const resource = useResource - .with({ count }) + .withState({ count }) .create(({ count }) => TestResource.initial(count)) .update((resource, { count }) => resource.transition("updated", count)) .on({ @@ -136,7 +136,7 @@ testModes("useResource (nested, stability across remounting)", (mode) => { const [count, setCount] = useState(0); const resource = useResource - .with({ count }) + .withState({ count }) .create(({ count }) => TestResource.initial(count)) .update((resource, { count }) => resource.transition("updated", count)) .on({ diff --git a/package.json b/package.json index 1a348389..b1712330 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "types": "src/index.ts", "main": "src/index.ts", "private": true, + "license": "MIT", "publishConfig": { "registry": "http://localhost:4873/" }, @@ -15,6 +16,7 @@ "serve": "vite preview", "test": "vitest", "typecheck": "tsc --build", + "test:prod": "vitest --mode production", "demos:react:store": "vite --port 3001 -c ./demos/react-store/vite.config.ts" }, "devDependencies": { @@ -23,6 +25,7 @@ "@domtree/any": "workspace:*", "@domtree/flavors": "workspace:*", "@domtree/minimal": "workspace:*", + "@import-meta-env/unplugin": "^0.1.8", "@rollup/plugin-sucrase": "^4.0.4", "@rollup/plugin-typescript": "^8.3.3", "@swc/core": "^1.2.203", @@ -33,6 +36,7 @@ "@typescript-eslint/eslint-plugin": "^5.27.0", "@typescript-eslint/parser": "^5.27.0", "@vitest/ui": "^0.14.1", + "dotenv": "^16.0.1", "esbuild": "^0.14.46", "eslint": "^8.16.0", "eslint-config-prettier": "^8.5.0", @@ -45,6 +49,7 @@ "esno": "^0.16.3", "fast-glob": "^3.2.11", "jsdom": "^19.0.0", + "magic-string": "^0.26.2", "postcss": "^8.4.14", "prettier": "^2.6.2", "rollup": "^2.75.6", diff --git a/packages/env.d.ts b/packages/env.d.ts new file mode 100644 index 00000000..cd62cb9d --- /dev/null +++ b/packages/env.d.ts @@ -0,0 +1,5 @@ +/// + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/verify/index.ts b/packages/verify/index.ts index 833c63d2..b4949809 100644 --- a/packages/verify/index.ts +++ b/packages/verify/index.ts @@ -10,10 +10,14 @@ export { } from "./src/assertions/basic.js"; export { isOneOf } from "./src/assertions/multi.js"; export { type TypeOf, hasType } from "./src/assertions/types.js"; -export { - type Expectation, - expected, - VerificationError, - verified, - verify, -} from "./src/verify.js"; +export { type Expectation, expected, VerificationError } from "./src/verify.js"; + +import { verify as verifyDev } from "./src/verify.js"; +export const verify: typeof verifyDev["noop"] = import.meta.env.DEV + ? verifyDev + : verifyDev.noop; + +import { verified as verifiedDev } from "./src/verify.js"; +export const verified: typeof verifiedDev["noop"] = import.meta.env.DEV + ? verifiedDev + : verifiedDev.noop; diff --git a/packages/verify/src/verify.ts b/packages/verify/src/verify.ts index 7cb43e86..1a1df243 100644 --- a/packages/verify/src/verify.ts +++ b/packages/verify/src/verify.ts @@ -35,6 +35,14 @@ export function verify( } } +verify.noop = ( + value: T, + _check: ((input: T) => input is U) | ((input: T) => boolean), + _error?: Expectation +): asserts value is U => { + return; +}; + export function verified( value: T, check: (input: T) => input is U, @@ -44,6 +52,14 @@ export function verified( return value; } +verified.noop = ( + value: T, + _check: (input: T) => input is U, + _error?: Expectation +): U => { + return value as U; +}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export class Expectation { static create(description?: string) { diff --git a/packages/verify/tests/basic.spec.ts b/packages/verify/tests/basic.spec.ts index 40181b14..a6fefbdc 100644 --- a/packages/verify/tests/basic.spec.ts +++ b/packages/verify/tests/basic.spec.ts @@ -3,7 +3,9 @@ import "./support.js"; import { expected, isEqual, isPresent, verify } from "@starbeam/verify"; import { describe, expect, test } from "vitest"; -describe("basic verification", () => { +const isProd = import.meta.env.PROD; + +describe.skipIf(isProd)("basic verification", () => { test("isPresent", () => { expect((value: unknown) => verify(value, isPresent)).toFail( null, diff --git a/packages/verify/tests/verify.spec.ts b/packages/verify/tests/verify.spec.ts index 44bdf5e3..dbc76f4a 100644 --- a/packages/verify/tests/verify.spec.ts +++ b/packages/verify/tests/verify.spec.ts @@ -1,7 +1,9 @@ import { expected, verify } from "@starbeam/verify"; import { describe, expect, test } from "vitest"; -describe("verify", () => { +const isProd = import.meta.env.PROD; + +describe.skipIf(isProd)("verify", () => { test("default verify message", () => { const isPresent = (input: T | null | undefined): input is T => { return input !== null && input !== undefined; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79992257..9f966444 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,7 @@ importers: '@domtree/any': workspace:* '@domtree/flavors': workspace:* '@domtree/minimal': workspace:* + '@import-meta-env/unplugin': ^0.1.8 '@rollup/plugin-sucrase': ^4.0.4 '@rollup/plugin-typescript': ^8.3.3 '@swc/core': ^1.2.203 @@ -29,6 +30,7 @@ importers: '@typescript-eslint/eslint-plugin': ^5.27.0 '@typescript-eslint/parser': ^5.27.0 '@vitest/ui': ^0.14.1 + dotenv: ^16.0.1 esbuild: ^0.14.46 eslint: ^8.16.0 eslint-config-prettier: ^8.5.0 @@ -41,6 +43,7 @@ importers: esno: ^0.16.3 fast-glob: ^3.2.11 jsdom: ^19.0.0 + magic-string: ^0.26.2 postcss: ^8.4.14 prettier: ^2.6.2 rollup: ^2.75.6 @@ -60,6 +63,7 @@ importers: '@domtree/any': link:@types/@domtree/any '@domtree/flavors': link:@types/@domtree/flavors '@domtree/minimal': link:@types/@domtree/minimal + '@import-meta-env/unplugin': 0.1.8_qyqavhxsy6dzc3xcfnsab7uxuy '@rollup/plugin-sucrase': 4.0.4_rollup@2.75.6 '@rollup/plugin-typescript': 8.3.3_rqgbr5uopfiucphy7ckzhieyka '@swc/core': 1.2.203 @@ -70,6 +74,7 @@ importers: '@typescript-eslint/eslint-plugin': 5.28.0_p7dpmbng2fwzidrcb7adceb33q '@typescript-eslint/parser': 5.28.0_aycqmkwbwxl3m4pnvl44sjixha '@vitest/ui': 0.14.2 + dotenv: 16.0.1 esbuild: 0.14.46 eslint: 8.18.0 eslint-config-prettier: 8.5.0_eslint@8.18.0 @@ -82,6 +87,7 @@ importers: esno: 0.16.3 fast-glob: 3.2.11 jsdom: 19.0.0 + magic-string: 0.26.2 postcss: 8.4.14 prettier: 2.7.1 rollup: 2.75.6 @@ -1719,6 +1725,23 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@import-meta-env/unplugin/0.1.8_qyqavhxsy6dzc3xcfnsab7uxuy: + resolution: {integrity: sha512-BSMxFokncqHijsdMztRg/xv20LAr5FdJ+hhKFZ1wJNYRhSoF28DsMlQ119B52XKWycj5okuAQO4Bh69Lb7729Q==} + engines: {node: ^12.20.0 || >= 14} + peerDependencies: + dotenv: ^11.0.0 || ^12.0.4 || ^13.0.1 || ^14.3.2 || ^15.0.1 || ^16.0.0 + dependencies: + dotenv: 16.0.1 + object-hash: 3.0.0 + picocolors: 1.0.0 + unplugin: 0.3.3_alb2oewjl62jflpa6tdsfn4mjy + transitivePeerDependencies: + - esbuild + - rollup + - vite + - webpack + dev: true + /@istanbuljs/load-nyc-config/1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -3776,6 +3799,11 @@ packages: domhandler: 5.0.3 dev: true + /dotenv/16.0.1: + resolution: {integrity: sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==} + engines: {node: '>=12'} + dev: true + /electron-to-chromium/1.4.160: resolution: {integrity: sha512-O1Z12YfyeX2LXYO7MdHIPazGXzLzQnr1ADW55U2ARQsJBPgfpJz3u+g3Mo2l1wSyfOCdiGqaX9qtV4XKZ0HNRA==} dev: true @@ -6705,6 +6733,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-hash/3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect/1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} dev: true @@ -8504,6 +8537,29 @@ packages: - webpack dev: true + /unplugin/0.3.3_alb2oewjl62jflpa6tdsfn4mjy: + resolution: {integrity: sha512-WjZWpUqqcYPQ/efR00Zm2m1+J1LitwoZ4uhHV4VdZ+IpW0Nh/qnDYtVf+nLhozXdGxslMPecOshVR7NiWFl4gA==} + peerDependencies: + esbuild: '>=0.13' + rollup: ^2.50.0 + vite: ^2.3.0 + webpack: 4 || 5 + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + dependencies: + esbuild: 0.14.46 + rollup: 2.75.6 + vite: 2.9.12 + webpack-virtual-modules: 0.4.3 + dev: true + /unplugin/0.6.3_alb2oewjl62jflpa6tdsfn4mjy: resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==} peerDependencies: diff --git a/rollup.config.js b/rollup.config.js index 547de676..ffabb199 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,4 +1,5 @@ -import glob from "fast-glob"; +/// +import { sync as glob } from "fast-glob"; import { readFileSync } from "fs"; import { dirname, resolve } from "path"; import { defineConfig } from "rollup"; @@ -6,15 +7,24 @@ import postcss from "rollup-plugin-postcss"; import ts from "rollup-plugin-ts"; import { fileURLToPath } from "url"; +import importMetaPlugin from "./.build/import-meta.js"; + const dir = fileURLToPath(import.meta.url); const root = dirname(resolve(dir)); -const packages = glob - .sync([ - resolve(root, "packages/*/package.json"), - resolve(root, "framework/*/*/package.json"), - ]) - .map((path) => [path, JSON.parse(readFileSync(path, "utf8"))]) +/** @typedef {{main: string; private: boolean; name: string}} PackageJSON */ + +const packages = glob([ + resolve(root, "packages/*/package.json"), + resolve(root, "framework/*/*/package.json"), +]) + .map( + (path) => + /** @type {[string, PackageJSON]} */ ([ + path, + /** @type {PackageJSON} */ (JSON.parse(readFileSync(path, "utf8"))), + ]) + ) .filter(([, pkg]) => pkg.main && pkg.private !== true) .map(([path, pkg]) => { const root = dirname(path); @@ -26,13 +36,12 @@ export default packages.map((pkg) => input: pkg.main, output: [ { - // file: resolve(pkg.root, "dist", "index.js"), dir: resolve(pkg.root, "dist"), format: "es", sourcemap: true, }, { - file: resolve(pkg.root, "dist", "index.cjs"), + dir: resolve(pkg.root, "dist"), format: "cjs", sourcemap: true, exports: "named", @@ -40,6 +49,7 @@ export default packages.map((pkg) => ], external: (id) => !(id.startsWith(".") || id.startsWith("/")), plugins: [ + importMetaPlugin, postcss(), ts({ transpiler: "swc", diff --git a/tsconfig.json b/tsconfig.json index b67ceb86..52f97041 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,12 +30,12 @@ "packages", "framework", "demos", + "build/**/*.ts", ".scripts/**/*.ts", "vite.config.ts", "**/vite.config.ts", - "rollup.config.ts", "rollup.config.js", - "**/.eslintrc.cjs" + ".build/*.js" ], "exclude": [ "**/node_modules/**", diff --git a/vite.config.ts b/vite.config.ts index 9b880bc5..a476231f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,5 @@ -import { dirname, resolve } from "path"; -import { fileURLToPath } from "url"; import { defineConfig } from "vitest/config"; -const dir = fileURLToPath(import.meta.url); -const root = dirname(resolve(dir)); - const BUNDLED_EXTERNAL_PACKAGES = [ "stacktracey", "as-table",