From cf15e9bbfa0b855b20a7734db065b5eb11aefef2 Mon Sep 17 00:00:00 2001 From: r17x Date: Thu, 10 Oct 2024 17:27:20 +0700 Subject: [PATCH 1/3] chore(unplugin-flag): setup project --- packages/unplugin-flag/.releaserc.json | 3 + packages/unplugin-flag/LICENSE | 21 + packages/unplugin-flag/README.md | 383 ++++++++++++++++++ packages/unplugin-flag/client.d.ts | 1 + packages/unplugin-flag/package.json | 155 +++++++ packages/unplugin-flag/src/astro.ts | 15 + packages/unplugin-flag/src/core/index.test.ts | 5 + packages/unplugin-flag/src/core/index.ts | 40 ++ packages/unplugin-flag/src/core/types.ts | 30 ++ packages/unplugin-flag/src/esbuild.ts | 4 + packages/unplugin-flag/src/farm.ts | 4 + packages/unplugin-flag/src/index.ts | 21 + packages/unplugin-flag/src/rolldown.ts | 4 + packages/unplugin-flag/src/rollup.ts | 4 + packages/unplugin-flag/src/rspack.ts | 4 + packages/unplugin-flag/src/vite.ts | 4 + packages/unplugin-flag/src/webpack.ts | 4 + packages/unplugin-flag/tsconfig.json | 13 + packages/unplugin-flag/tsup.config.ts | 11 + yarn.lock | 47 +++ 20 files changed, 773 insertions(+) create mode 100644 packages/unplugin-flag/.releaserc.json create mode 100644 packages/unplugin-flag/LICENSE create mode 100644 packages/unplugin-flag/README.md create mode 100644 packages/unplugin-flag/client.d.ts create mode 100644 packages/unplugin-flag/package.json create mode 100644 packages/unplugin-flag/src/astro.ts create mode 100644 packages/unplugin-flag/src/core/index.test.ts create mode 100644 packages/unplugin-flag/src/core/index.ts create mode 100644 packages/unplugin-flag/src/core/types.ts create mode 100644 packages/unplugin-flag/src/esbuild.ts create mode 100644 packages/unplugin-flag/src/farm.ts create mode 100644 packages/unplugin-flag/src/index.ts create mode 100644 packages/unplugin-flag/src/rolldown.ts create mode 100644 packages/unplugin-flag/src/rollup.ts create mode 100644 packages/unplugin-flag/src/rspack.ts create mode 100644 packages/unplugin-flag/src/vite.ts create mode 100644 packages/unplugin-flag/src/webpack.ts create mode 100644 packages/unplugin-flag/tsconfig.json create mode 100644 packages/unplugin-flag/tsup.config.ts diff --git a/packages/unplugin-flag/.releaserc.json b/packages/unplugin-flag/.releaserc.json new file mode 100644 index 0000000..9dee895 --- /dev/null +++ b/packages/unplugin-flag/.releaserc.json @@ -0,0 +1,3 @@ +{ + "extends": "@r17x/semantic-release" +} diff --git a/packages/unplugin-flag/LICENSE b/packages/unplugin-flag/LICENSE new file mode 100644 index 0000000..dd3fb7c --- /dev/null +++ b/packages/unplugin-flag/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Rin + +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/packages/unplugin-flag/README.md b/packages/unplugin-flag/README.md new file mode 100644 index 0000000..2f57fc0 --- /dev/null +++ b/packages/unplugin-flag/README.md @@ -0,0 +1,383 @@ +
+

unplugin-flag

+

+ A powerfull plugin for feature flag. +

+

+ + npm package + npm package downloads + +

+

Supports:

+

+ Next.js + Vite + Esbuild + Webpack + Astro + Rollup + Rolldown + Rspack + Farm +

+
+ +## Table of Contents + +* [📚 Table of Contents](#-table-of-contents) +* [🔥 Features](#features) +* [📦 Installation](installation) +* [🚀 Basic Usage](#basic-usage) + * [1. Configuration](#configuration) + * [2. Flag your code](#accessing-environment-variables) + * [3. Done](#) +* [💡 Acknowledgements](#acknowledgements) + +## Features + + +## Installation + +Install via your preferred package manager: + +```bash +npm install unplugin-flag # npm + +yarn add unplugin-flag # yarn + +bun add unplugin-flag # bun + +pnpm add unplugin-flag # pnpm +``` +
+ Back to top +
+ +## Basic Usage + +### Configuration + +
+Next.js
+ +```ts +// next.config.mjs +import Environment from 'unplugin-flag/webpack' + +const nextConfig = { + webpack(config){ + config.plugins.push(Environment('PREFIX_APP')) + return config + }, +} + +export default nextConfig +``` +
+ Back to top +
+ +
+ + +
+Vite
+ +```ts +// vite.config.ts +import Environment from 'unplugin-flag/vite' + +export default defineConfig({ + plugins: [ + Environment('PREFIX_APP'), + ], +}) +``` +
+ Back to top +
+ +
+ +
+Farm
+ +```ts +// farm.config.ts +import Environment from 'unplugin-flag/farm' + +export default defineconfig({ + plugins: [ + Environment('PREFIX_APP'), + ], +}) +``` +
+ Back to top +
+ +
+ +
+Rspack
+ +```ts +// rspack.config.js +module.exports = { + /* ... */ + plugins: [ + require('unplugin-flag/rspack')('PREFIX_APP') + ] +} +``` +
+ Back to top +
+ +
+ + +
+Rollup
+ +```ts +// rollup.config.js +import Environment from 'unplugin-flag/rollup' + +export default { + plugins: [ + Environment('PREFIX_APP'), + ], +} +``` + +
+ Back to top +
+ +
+ + +
+Rolldown
+ +```ts +// rolldown.config.js +import Environment from 'unplugin-flag/rolldown' + +export default { + plugins: [ + Environment('PREFIX_APP'), + ], +} +``` + +
+ Back to top +
+ +
+ + +
+Webpack
+ +```ts +// webpack.config.js +module.exports = { + /* ... */ + plugins: [ + require('unplugin-flag/webpack')("PREFIX_APP") + ] +} +``` + +
+ Back to top +
+ +
+ +
+Esbuild
+ +```ts +// esbuild.config.js +import { build } from 'esbuild' +import Environment from 'unplugin-flag/esbuild' + +build({ + plugins: [Environment('PREFIX_APP')], +}) +``` + +
+ Back to top +
+ +
+ +
+Astro
+ +```ts +// astro.config.mjs +import { defineConfig } from 'astro/config' +import Environment from 'unplugin-flag/astro' + +build({ + plugins: [Environment('PREFIX_APP')], +}) +``` + +
+ Back to top +
+ +
+ +#### Schema Validation + +Use the `schema` option with [zod](https://github.com/colinhacks/zod_) for validating environment variables. This automatically creates a virtual module with types. + + +```ts +Environment({ + match: 'PREFIX_', // or ['PREFIX_', 'PREFIX2_'] + schema: { + PREFIX_APP_NAME: z.string().min(1).default('My App'), + PREFIX_APP_PORT: z.coerce.number().min(1).default(3000), + }, +}) +``` +
+ Back to top +
+ +#### Intellisense with TypeScript +To enable Intellisense for environment variables, add the following to your `tsconfig.json`: + +```json +{ + "compilerOptions": { + "types": ["unplugin-flag/client"] + } +} +``` +
+ Back to top +
+ +### Accessing Environment Variables + +You can access environment variables from the virtual module `@env`: + +```typescript +import { env } from '@env' + +console.log(env.PREFIX_APP_NAME) +``` + +If you want to customize the module name, use the `moduleEnvName` option: + +```typescript +// in plugin configuration +Environment({ + match: 'PREFIX_', // or ['PREFIX_', 'PREFIX2_'] + schema: ..., + moduleEnvName: 'MYENV', +}) + + +// you can access it from `MYENV` module +import { env } from 'MYENV' + +console.log(env.PREFIX_APP_NAME) +``` + +
+ Back to top +
+ +### Client/Server Environment + +To handle environment variables separately for client and server, use the client and server options. This allows for precise control over which variables are accessible in different environments. + +> [!NOTE] +> When using the client and server options, you cannot access environment variables through the @env module. Instead, use `@env/client` for client-side variables and `@env/server` for server-side variables by default. + +Example configuration: +```ts +Environment({ + client: { + match: 'CLIENT_', + schema: { + CLIENT_APP_NAME: z.string().min(1).default('My App'), + }, + }, + server: { + match: 'SERVER_', + schema: { + SERVER_APP_DB_URL: z.string().min(1).default('postgres://localhost:5432/mydb'), + } + }, +}) +``` + +If you'd like to change the default module names `@env/client` and `@env/server`, you can use the optional `moduleEnvName` key to define a custom module name for accessing the environment variables. + +> [!CAUTION] +> When customizing moduleEnvName for client and server, ensure the module names are different. Using the same name for both client and server can cause conflicts and unpredictable behavior. + +```ts +Environment({ + client: { + match: 'CLIENT_', + schema: { + CLIENT_APP_NAME: z.string().min(1).default('My App'), + }, + moduleEnvName: '@myenv/client', // Optional: Customize the client module name + }, + server: { + match: 'SERVER_', + schema: { + SERVER_APP_DB_URL: z.string().min(1).default('postgres://localhost:5432/mydb'), + }, + moduleEnvName: '@myenv/server', // Optional: Customize the server module name + }, +}) +``` + +#### Accessing Client/Server Environment + +```ts +// client environment +import { env } from '@env/client' + +env.CLIENT_APP_NAME // typed with string + +// server environment +import { env } from '@env/server' + +env.SERVER_APP_DB_URL // typed with string + +``` + + +
+ Back to top +
+ +## Acknowledgements + +* [dotenv](https://github.com/motdotla/dotenv) +* [t3-env](https://github.com/t3-oss/t3-env) +* [vite-plugin-environment](https://github.com/ElMassimo/vite-plugin-environment) + +
+ +
+ Back to top +
+ diff --git a/packages/unplugin-flag/client.d.ts b/packages/unplugin-flag/client.d.ts new file mode 100644 index 0000000..12255ad --- /dev/null +++ b/packages/unplugin-flag/client.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/unplugin-flag/package.json b/packages/unplugin-flag/package.json new file mode 100644 index 0000000..aab8d7f --- /dev/null +++ b/packages/unplugin-flag/package.json @@ -0,0 +1,155 @@ +{ + "name": "unplugin-flag", + "type": "module", + "version": "0.0.0", + "description": "Simple plugin for feature flag", + "license": "MIT", + "author": "r17x ", + "homepage": "https://github.com/r17x/js/tree/main/packages/unplugin-flag#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/r17x/js.git", + "directory": "packages/unplugin-flag" + }, + "bugs": { + "url": "https://github.com/r17x/js/issues" + }, + "keywords": [ + "plugins", + "unplugin", + "vite", + "webpack", + "rollup", + "transform", + "farm", + "environment", + "env", + "dotenv", + "te-env" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./astro": { + "types": "./dist/astro.d.ts", + "import": "./dist/astro.js", + "require": "./dist/astro.cjs" + }, + "./rspack": { + "types": "./dist/rspack.d.ts", + "import": "./dist/rspack.js", + "require": "./dist/rspack.cjs" + }, + "./farm": { + "types": "./dist/farm.d.ts", + "import": "./dist/farm.js", + "require": "./dist/farm.cjs" + }, + "./vite": { + "types": "./dist/vite.d.ts", + "import": "./dist/vite.js", + "require": "./dist/vite.cjs" + }, + "./webpack": { + "types": "./dist/webpack.d.ts", + "import": "./dist/webpack.js", + "require": "./dist/webpack.cjs" + }, + "./rollup": { + "types": "./dist/rollup.d.ts", + "import": "./dist/rollup.js", + "require": "./dist/rollup.cjs" + }, + "./rolldown": { + "types": "./dist/rolldown.d.ts", + "import": "./dist/rolldown.js", + "require": "./dist/rolldown.cjs" + }, + "./esbuild": { + "types": "./dist/esbuild.d.ts", + "import": "./dist/esbuild.js", + "require": "./dist/esbuild.cjs" + }, + "./types": { + "types": "./dist/types.d.ts", + "import": "./dist/types.js", + "require": "./dist/types.cjs" + }, + "./*": "./*" + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": ["./dist/*", "./*"] + } + }, + "files": ["dist", "client.d.ts"], + "scripts": { + "prepack": "npm run build", + "build": "tsup", + "build:fix": "esno ../../tooling/unplugin/postbuild.ts", + "dev": "tsup --watch src", + "postversion": "biome check --write" + }, + "peerDependencies": { + "@farmfe/core": ">=1", + "astro": ">=4", + "esbuild": "*", + "rolldown": "^0", + "rollup": "^3", + "vite": ">=3", + "webpack": "^4 || ^5" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + }, + "dependencies": { + "@mobily/ts-belt": "4.0.0-rc.1", + "@rollup/pluginutils": "^5.1.0", + "npm-run-path": "^5.3.0", + "typescript": "^5.3.2", + "unplugin": "1.14.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.2", + "@rspack/core": "^1.0.7", + "astro": "^4.11.1", + "bumpp": "^9.2.0", + "chalk": "^5.3.0", + "eslint": "^8.55.0", + "esno": "^4.0.0", + "fast-glob": "^3.3.2", + "rolldown": "^0.13.2", + "rollup": "^4.6.1", + "tsup": "^8.0.1", + "vite": "^5.0.4", + "webpack": "^5.89.0" + }, + "publishConfig": { + "provenance": false, + "access": "public" + } +} diff --git a/packages/unplugin-flag/src/astro.ts b/packages/unplugin-flag/src/astro.ts new file mode 100644 index 0000000..c6b7282 --- /dev/null +++ b/packages/unplugin-flag/src/astro.ts @@ -0,0 +1,15 @@ +import type { AstroIntegration } from "astro"; +import type { PluginOption } from "./core/types"; +import Environment from "./vite"; + +export default function (options: PluginOption): AstroIntegration { + return { + name: "unplugin-flag", + hooks: { + "astro:config:setup": async (astro) => { + astro.config.vite.plugins ||= []; + astro.config.vite.plugins.push(Environment(options)); + }, + }, + }; +} diff --git a/packages/unplugin-flag/src/core/index.test.ts b/packages/unplugin-flag/src/core/index.test.ts new file mode 100644 index 0000000..c0c9f6d --- /dev/null +++ b/packages/unplugin-flag/src/core/index.test.ts @@ -0,0 +1,5 @@ +import { describe, it } from "vitest"; + +describe("uplugin-environment:core", () => { + it.todo("fist core"); +}); diff --git a/packages/unplugin-flag/src/core/index.ts b/packages/unplugin-flag/src/core/index.ts new file mode 100644 index 0000000..418b10a --- /dev/null +++ b/packages/unplugin-flag/src/core/index.ts @@ -0,0 +1,40 @@ +import { D, R, flow, pipe } from "@mobily/ts-belt"; +import { z } from "zod"; +import { name as pkgName } from "../../package.json"; +import type * as CoreType from "./types"; + +const binaryValue = z.boolean().or(z.enum(["on", "off"])); +const unaryFunction = z.function(); +const optionValue = binaryValue + .or(unaryFunction.returns(z.promise(binaryValue))) + .or(unaryFunction.returns(binaryValue)); +export const option = z.record(optionValue); + +const defaultFactory: CoreType.Data["factory"] = { + name: pkgName, + enforce: "pre", +}; + +// TODO: +// * handle virtual modules +// * dead-code elimination by value of record `options` +// * handle function as value when return promise binary value +// * handle function as value when return binary value +const createFactory = (_options: CoreType.PluginOption) => defaultFactory; + +const getOptionsR = (opt: CoreType.PluginOption) => + R.fromExecution(() => option.parse(opt)); + +export const unpluginFactory = flow( + getOptionsR, + R.map((options) => + pipe( + D.makeEmpty(), + D.set("options", options), + D.set("factory", createFactory(options)), + ), + ), + // R.map(factory), + // R.map(updateFactory), + R.mapWithDefault(defaultFactory, (data) => data.factory), +); diff --git a/packages/unplugin-flag/src/core/types.ts b/packages/unplugin-flag/src/core/types.ts new file mode 100644 index 0000000..d7b2183 --- /dev/null +++ b/packages/unplugin-flag/src/core/types.ts @@ -0,0 +1,30 @@ +import type { UnpluginFactory, UnpluginOptions } from "unplugin"; + +export type BinaryValue = boolean | "on" | "off"; +export type PromiseLikeBinaryValue = PromiseLike; + +type UnaryFunction = () => T; + +type FlagValue = + | BinaryValue + | UnaryFunction + | UnaryFunction; + +/** + * @example + * ```ts + * Flag({ + * USER_PROFILE_V2: 'on', + * USER_PROFILE_V3: () => 'on', + * USER_PROFILE_V4: () => Promise.resolve('on'), + * }) + * ``` + */ +export type PluginOption = Record; + +export type UnpluginEnvironmentFactory = UnpluginFactory; + +export type Data = { + options: PluginOption; + factory: UnpluginOptions; +}; diff --git a/packages/unplugin-flag/src/esbuild.ts b/packages/unplugin-flag/src/esbuild.ts new file mode 100644 index 0000000..9bf8552 --- /dev/null +++ b/packages/unplugin-flag/src/esbuild.ts @@ -0,0 +1,4 @@ +import { createEsbuildPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createEsbuildPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/farm.ts b/packages/unplugin-flag/src/farm.ts new file mode 100644 index 0000000..40b5861 --- /dev/null +++ b/packages/unplugin-flag/src/farm.ts @@ -0,0 +1,4 @@ +import { createFarmPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createFarmPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/index.ts b/packages/unplugin-flag/src/index.ts new file mode 100644 index 0000000..9b07151 --- /dev/null +++ b/packages/unplugin-flag/src/index.ts @@ -0,0 +1,21 @@ +import { createUnplugin } from "unplugin"; +import { unpluginFactory } from "./core"; + +export { unpluginFactory }; +/** + * @example + * ```js + * import { Flag } from 'unplugin-flag' + * + * Flag({ + * // ANY for NAME: BINARY_VALUE + * USER_PROFILE_V1: 'off', + * USER_PROFILE_V2: 'on', + * USER_PROFILE_V3: () => 'on', + * USER_PROFILE_V4: () => Promise.resolve('on'), + * }) + * + * ``` + */ +export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory); +export default unplugin; diff --git a/packages/unplugin-flag/src/rolldown.ts b/packages/unplugin-flag/src/rolldown.ts new file mode 100644 index 0000000..b5c948b --- /dev/null +++ b/packages/unplugin-flag/src/rolldown.ts @@ -0,0 +1,4 @@ +import { createRolldownPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createRolldownPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/rollup.ts b/packages/unplugin-flag/src/rollup.ts new file mode 100644 index 0000000..cb21aad --- /dev/null +++ b/packages/unplugin-flag/src/rollup.ts @@ -0,0 +1,4 @@ +import { createRollupPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createRollupPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/rspack.ts b/packages/unplugin-flag/src/rspack.ts new file mode 100644 index 0000000..85ee203 --- /dev/null +++ b/packages/unplugin-flag/src/rspack.ts @@ -0,0 +1,4 @@ +import { createRspackPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createRspackPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/vite.ts b/packages/unplugin-flag/src/vite.ts new file mode 100644 index 0000000..69ebc10 --- /dev/null +++ b/packages/unplugin-flag/src/vite.ts @@ -0,0 +1,4 @@ +import { createVitePlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createVitePlugin(unpluginFactory); diff --git a/packages/unplugin-flag/src/webpack.ts b/packages/unplugin-flag/src/webpack.ts new file mode 100644 index 0000000..ca73fac --- /dev/null +++ b/packages/unplugin-flag/src/webpack.ts @@ -0,0 +1,4 @@ +import { createWebpackPlugin } from "unplugin"; +import { unpluginFactory } from "."; + +export default createWebpackPlugin(unpluginFactory); diff --git a/packages/unplugin-flag/tsconfig.json b/packages/unplugin-flag/tsconfig.json new file mode 100644 index 0000000..5e4be26 --- /dev/null +++ b/packages/unplugin-flag/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "lib": ["esnext", "DOM"], + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true + }, + "exclude": ["dist", "eslint.config.js"] +} diff --git a/packages/unplugin-flag/tsup.config.ts b/packages/unplugin-flag/tsup.config.ts new file mode 100644 index 0000000..b72cf62 --- /dev/null +++ b/packages/unplugin-flag/tsup.config.ts @@ -0,0 +1,11 @@ +import type { Options } from "tsup"; + +export default ({ + entryPoints: ["src/*.ts"], + clean: true, + format: ["cjs", "esm"], + dts: true, + cjsInterop: true, + splitting: true, + onSuccess: "npm run build:fix", +}); diff --git a/yarn.lock b/yarn.lock index c79e244..006bebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13852,6 +13852,53 @@ __metadata: languageName: unknown linkType: soft +"unplugin-flag@workspace:packages/unplugin-flag": + version: 0.0.0-use.local + resolution: "unplugin-flag@workspace:packages/unplugin-flag" + dependencies: + "@biomejs/biome": "npm:^1.9.2" + "@mobily/ts-belt": "npm:4.0.0-rc.1" + "@rollup/pluginutils": "npm:^5.1.0" + "@rspack/core": "npm:^1.0.7" + astro: "npm:^4.11.1" + bumpp: "npm:^9.2.0" + chalk: "npm:^5.3.0" + eslint: "npm:^8.55.0" + esno: "npm:^4.0.0" + fast-glob: "npm:^3.3.2" + npm-run-path: "npm:^5.3.0" + rolldown: "npm:^0.13.2" + rollup: "npm:^4.6.1" + tsup: "npm:^8.0.1" + typescript: "npm:^5.3.2" + unplugin: "npm:1.14.1" + vite: "npm:^5.0.4" + webpack: "npm:^5.89.0" + zod: "npm:^3.23.8" + peerDependencies: + "@farmfe/core": ">=1" + astro: ">=4" + esbuild: "*" + rolldown: ^0 + rollup: ^3 + vite: ">=3" + webpack: ^4 || ^5 + peerDependenciesMeta: + "@farmfe/core": + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + languageName: unknown + linkType: soft + "unplugin-rescript@workspace:packages/unplugin-rescript": version: 0.0.0-use.local resolution: "unplugin-rescript@workspace:packages/unplugin-rescript" From 588d23bb24eb7ac34662226e33e7518cdae435b6 Mon Sep 17 00:00:00 2001 From: r17x Date: Sat, 12 Oct 2024 12:50:05 +0700 Subject: [PATCH 2/3] chore: update type definitions --- packages/unplugin-flag/src/core/index.ts | 2 +- packages/unplugin-flag/src/core/types.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/unplugin-flag/src/core/index.ts b/packages/unplugin-flag/src/core/index.ts index 418b10a..3d35926 100644 --- a/packages/unplugin-flag/src/core/index.ts +++ b/packages/unplugin-flag/src/core/index.ts @@ -20,7 +20,7 @@ const defaultFactory: CoreType.Data["factory"] = { // * dead-code elimination by value of record `options` // * handle function as value when return promise binary value // * handle function as value when return binary value -const createFactory = (_options: CoreType.PluginOption) => defaultFactory; +const createFactory: CoreType.CreateFactory = (_options) => defaultFactory; const getOptionsR = (opt: CoreType.PluginOption) => R.fromExecution(() => option.parse(opt)); diff --git a/packages/unplugin-flag/src/core/types.ts b/packages/unplugin-flag/src/core/types.ts index d7b2183..c66d147 100644 --- a/packages/unplugin-flag/src/core/types.ts +++ b/packages/unplugin-flag/src/core/types.ts @@ -24,6 +24,8 @@ export type PluginOption = Record; export type UnpluginEnvironmentFactory = UnpluginFactory; +export type CreateFactory = (options: PluginOption) => UnpluginOptions; + export type Data = { options: PluginOption; factory: UnpluginOptions; From 331674f051d4e5aa28aa618c5d2752ec2cdbab36 Mon Sep 17 00:00:00 2001 From: r17x Date: Tue, 15 Oct 2024 01:32:24 +0700 Subject: [PATCH 3/3] chore: temp --- .../src/core => shared}/utils.ts | 0 .../unplugin-environment/src/core/index.ts | 2 +- packages/unplugin-environment/tsconfig.json | 33 ++-- packages/unplugin-flag/README.md | 156 ++++-------------- packages/unplugin-flag/src/core/ast.ts | 125 ++++++++++++++ packages/unplugin-flag/src/core/index.ts | 87 ++++++++-- packages/unplugin-flag/src/core/types.ts | 13 +- packages/unplugin-flag/tsconfig.json | 33 ++-- playground/index.html | 1 + playground/src/index.ts | 9 + playground/tsconfig.json | 9 +- playground/vite.config.mts | 15 +- 12 files changed, 305 insertions(+), 178 deletions(-) rename packages/{unplugin-environment/src/core => shared}/utils.ts (100%) create mode 100644 packages/unplugin-flag/src/core/ast.ts create mode 100644 playground/src/index.ts diff --git a/packages/unplugin-environment/src/core/utils.ts b/packages/shared/utils.ts similarity index 100% rename from packages/unplugin-environment/src/core/utils.ts rename to packages/shared/utils.ts diff --git a/packages/unplugin-environment/src/core/index.ts b/packages/unplugin-environment/src/core/index.ts index 0027f75..04f0fb2 100644 --- a/packages/unplugin-environment/src/core/index.ts +++ b/packages/unplugin-environment/src/core/index.ts @@ -9,7 +9,7 @@ import { zodToTs } from "zod-to-ts"; import { name as pkgName, version as pkgVersion } from "../../package.json"; import { log } from "./logger"; import type * as CoreType from "./types"; -import { dropHead, exclaim, toJsonString, toNull, toUndefined } from "./utils"; +import { dropHead, exclaim, toJsonString, toNull, toUndefined } from "@utils"; const defaultFactory: CoreType.Data["factory"] = { name: pkgName, diff --git a/packages/unplugin-environment/tsconfig.json b/packages/unplugin-environment/tsconfig.json index 5e4be26..eeda256 100644 --- a/packages/unplugin-environment/tsconfig.json +++ b/packages/unplugin-environment/tsconfig.json @@ -1,13 +1,24 @@ { - "compilerOptions": { - "target": "es2020", - "lib": ["esnext", "DOM"], - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "strict": true, - "strictNullChecks": true, - "esModuleInterop": true - }, - "exclude": ["dist", "eslint.config.js"] + "compilerOptions": { + "paths": { + "@utils": [ + "./../shared/utils.ts" + ] + }, + "target": "es2020", + "lib": [ + "esnext", + "DOM" + ], + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true + }, + "exclude": [ + "dist", + "eslint.config.js" + ] } diff --git a/packages/unplugin-flag/README.md b/packages/unplugin-flag/README.md index 2f57fc0..08c96db 100644 --- a/packages/unplugin-flag/README.md +++ b/packages/unplugin-flag/README.md @@ -1,7 +1,7 @@

unplugin-flag

- A powerfull plugin for feature flag. + A plugin for feature flag.

@@ -30,12 +30,15 @@ * [📦 Installation](installation) * [🚀 Basic Usage](#basic-usage) * [1. Configuration](#configuration) - * [2. Flag your code](#accessing-environment-variables) + * [2. Flag your code](#flag-your-code) * [3. Done](#) * [💡 Acknowledgements](#acknowledgements) ## Features +* ✍️ **Type-Safe**: Automatically inferred types based on your feature flag configuration. +* ⚡ **Developer-Friendly**: Lightweight and simple API for managing feature flag. +* **💀 Dead Code Elimination**: Removed unused code flag. ## Installation @@ -63,11 +66,11 @@ pnpm add unplugin-flag # pnpm ```ts // next.config.mjs -import Environment from 'unplugin-flag/webpack' +import Flag from 'unplugin-flag/webpack' const nextConfig = { webpack(config){ - config.plugins.push(Environment('PREFIX_APP')) + config.plugins.push(Flag('PREFIX_APP')) return config }, } @@ -86,11 +89,11 @@ export default nextConfig ```ts // vite.config.ts -import Environment from 'unplugin-flag/vite' +import Flag from 'unplugin-flag/vite' export default defineConfig({ plugins: [ - Environment('PREFIX_APP'), + Flag('PREFIX_APP'), ], }) ``` @@ -105,11 +108,11 @@ export default defineConfig({ ```ts // farm.config.ts -import Environment from 'unplugin-flag/farm' +import Flag from 'unplugin-flag/farm' export default defineconfig({ plugins: [ - Environment('PREFIX_APP'), + Flag('PREFIX_APP'), ], }) ``` @@ -143,11 +146,11 @@ module.exports = { ```ts // rollup.config.js -import Environment from 'unplugin-flag/rollup' +import Flag from 'unplugin-flag/rollup' export default { plugins: [ - Environment('PREFIX_APP'), + Flag('PREFIX_APP'), ], } ``` @@ -164,11 +167,11 @@ export default { ```ts // rolldown.config.js -import Environment from 'unplugin-flag/rolldown' +import Flag from 'unplugin-flag/rolldown' export default { plugins: [ - Environment('PREFIX_APP'), + Flag('PREFIX_APP'), ], } ``` @@ -205,10 +208,10 @@ module.exports = { ```ts // esbuild.config.js import { build } from 'esbuild' -import Environment from 'unplugin-flag/esbuild' +import Flag from 'unplugin-flag/esbuild' build({ - plugins: [Environment('PREFIX_APP')], + plugins: [Flag('PREFIX_APP')], }) ``` @@ -224,10 +227,10 @@ build({ ```ts // astro.config.mjs import { defineConfig } from 'astro/config' -import Environment from 'unplugin-flag/astro' +import Flag from 'unplugin-flag/astro' build({ - plugins: [Environment('PREFIX_APP')], + plugins: [Flag('PREFIX_APP')], }) ``` @@ -237,24 +240,6 @@ build({
-#### Schema Validation - -Use the `schema` option with [zod](https://github.com/colinhacks/zod_) for validating environment variables. This automatically creates a virtual module with types. - - -```ts -Environment({ - match: 'PREFIX_', // or ['PREFIX_', 'PREFIX2_'] - schema: { - PREFIX_APP_NAME: z.string().min(1).default('My App'), - PREFIX_APP_PORT: z.coerce.number().min(1).default(3000), - }, -}) -``` -

- #### Intellisense with TypeScript To enable Intellisense for environment variables, add the following to your `tsconfig.json`: @@ -269,112 +254,31 @@ To enable Intellisense for environment variables, add the following to your `tsc Back to top
-### Accessing Environment Variables - -You can access environment variables from the virtual module `@env`: - -```typescript -import { env } from '@env' - -console.log(env.PREFIX_APP_NAME) -``` - -If you want to customize the module name, use the `moduleEnvName` option: - -```typescript -// in plugin configuration -Environment({ - match: 'PREFIX_', // or ['PREFIX_', 'PREFIX2_'] - schema: ..., - moduleEnvName: 'MYENV', -}) - - -// you can access it from `MYENV` module -import { env } from 'MYENV' - -console.log(env.PREFIX_APP_NAME) -``` - - - -### Client/Server Environment - -To handle environment variables separately for client and server, use the client and server options. This allows for precise control over which variables are accessible in different environments. - -> [!NOTE] -> When using the client and server options, you cannot access environment variables through the @env module. Instead, use `@env/client` for client-side variables and `@env/server` for server-side variables by default. +### Flag your code -Example configuration: ```ts -Environment({ - client: { - match: 'CLIENT_', - schema: { - CLIENT_APP_NAME: z.string().min(1).default('My App'), - }, - }, - server: { - match: 'SERVER_', - schema: { - SERVER_APP_DB_URL: z.string().min(1).default('postgres://localhost:5432/mydb'), - } - }, -}) -``` - -If you'd like to change the default module names `@env/client` and `@env/server`, you can use the optional `moduleEnvName` key to define a custom module name for accessing the environment variables. - -> [!CAUTION] -> When customizing moduleEnvName for client and server, ensure the module names are different. Using the same name for both client and server can cause conflicts and unpredictable behavior. +import { flag } from '@flag' -```ts -Environment({ - client: { - match: 'CLIENT_', - schema: { - CLIENT_APP_NAME: z.string().min(1).default('My App'), - }, - moduleEnvName: '@myenv/client', // Optional: Customize the client module name - }, - server: { - match: 'SERVER_', - schema: { - SERVER_APP_DB_URL: z.string().min(1).default('postgres://localhost:5432/mydb'), - }, - moduleEnvName: '@myenv/server', // Optional: Customize the server module name - }, -}) +const someFeature = flag.MY_FEATURE("show me when true", "show me when false") ``` -#### Accessing Client/Server Environment - -```ts -// client environment -import { env } from '@env/client' - -env.CLIENT_APP_NAME // typed with string - -// server environment -import { env } from '@env/server' - -env.SERVER_APP_DB_URL // typed with string +Result +```diff +// when true +-- const someFeature = flag.MY_FEATURE("show me when true", "show me when false") +++ const someFeature = "show me when true" +// when false +-- const someFeature = flag.MY_FEATURE("show me when true", "show me when false") +++ const someFeature = "show me when false" ``` - ## Acknowledgements -* [dotenv](https://github.com/motdotla/dotenv) -* [t3-env](https://github.com/t3-oss/t3-env) -* [vite-plugin-environment](https://github.com/ElMassimo/vite-plugin-environment) -
diff --git a/packages/unplugin-flag/src/core/ast.ts b/packages/unplugin-flag/src/core/ast.ts new file mode 100644 index 0000000..f8ab9af --- /dev/null +++ b/packages/unplugin-flag/src/core/ast.ts @@ -0,0 +1,125 @@ +import { A, D, F, flow, G } from "@mobily/ts-belt"; +import type { EliminationFlag, Data } from "./types"; +import * as ts from "typescript"; + +/** + * @description it will be used in runtime for check the flag value. + * @example + * ```ts + * // configuration + * { + * USER_PROFILE_V1: () => true, + * } + * + * // in user codebase + * import { flag } from 'moduleFlagName' + * const evaluation = flag.USER_PROFILE_V1("when On", "when Off") + * ``` + * */ +export const createFlagModule = flow( + F.identity, + D.getUnsafe("flagConfig"), + D.map( + F.ifElse( + G.isFunction, + (v) => v.toString(), + (v) => (G.isString(v) ? v === "on" : v).toString(), + ), + ), + D.toPairs, +); + +export const transform = ( + code: string, + id: string, + flags: EliminationFlag[] = [], +) => { + const source = ts.createSourceFile( + id, + code, // source code + ts.ScriptTarget.Latest, + true, + ); + if (flags.length === 0) { + return code; + } + + // Function to collect all named imports from a specific module + function getNamedImportsFromModule( + sourceFile: ts.SourceFile, + moduleName: string, + ): string[] { + const namedImports: string[] = []; + // Traverse the AST + function findImports(node: ts.Node) { + if (ts.isImportDeclaration(node)) { + const importClause = node.importClause; + const moduleSpecifier = node.moduleSpecifier + .getText() + .replace(/['"]/g, ""); // Remove quotes + + // Check if it's the module we're interested in + if ( + moduleSpecifier === moduleName && + importClause?.namedBindings && + ts.isNamedImports(importClause.namedBindings) + ) { + // Collect the named imports + importClause.namedBindings.elements.forEach((element) => { + // when use as import named. + if (element.propertyName) { + namedImports.push(element.propertyName.getText()); + } else { + namedImports.push(element.name.getText()); + } + }); + } + } + + ts.forEachChild(node, findImports); + } + + findImports(sourceFile); + return namedImports; + } + function elimination( + node: ts.SourceFile, + namedImports: string[] = [], + ): ts.SourceFile { + function visitor(node: ts.Node): ts.Node { + if (ts.isCallExpression(node)) { + const expression = node.expression; + // Check if it's a property access expression like `flag.HOME_V1` + if ( + ts.isPropertyAccessExpression(expression) && + node.arguments.length >= 1 + ) { + const objectName = expression.expression.getText(); // e.g. 'flag' + const methodName = expression.name.getText(); // e.g. 'HOME_V1' + if (namedImports.includes(objectName)) { + const flagged = flags.find((flag) => flag.key === methodName); + if (flagged) { + const [truthy, falsy] = node.arguments; + if (flagged.value) { + return truthy; + } + return falsy || ts.factory.createNull(); + } + } + } + } + // Continue visiting all child nodes + return ts.visitEachChild(node, visitor, undefined); + } + return ts.visitNode(node, visitor); + } + + const namedImports = getNamedImportsFromModule(source, "@flag"); + + // Update the AST by replacing nodes + const updatedSourceFile = elimination(source, namedImports); + + // Print the updated code + const printer = ts.createPrinter(); + return printer.printFile(updatedSourceFile); +}; diff --git a/packages/unplugin-flag/src/core/index.ts b/packages/unplugin-flag/src/core/index.ts index 3d35926..c834940 100644 --- a/packages/unplugin-flag/src/core/index.ts +++ b/packages/unplugin-flag/src/core/index.ts @@ -1,7 +1,12 @@ -import { D, R, flow, pipe } from "@mobily/ts-belt"; +import { A, D, F, G, N, O, R, S, flow, pipe } from "@mobily/ts-belt"; import { z } from "zod"; import { name as pkgName } from "../../package.json"; import type * as CoreType from "./types"; +import * as ast from "./ast"; +import * as utils from "@utils"; + +type U = ReturnType; +type FV = CoreType.FlagValue; const binaryValue = z.boolean().or(z.enum(["on", "off"])); const unaryFunction = z.function(); @@ -15,26 +20,78 @@ const defaultFactory: CoreType.Data["factory"] = { enforce: "pre", }; +const valToBoolean = F.ifElse< + readonly [string, FV], + O.Option<{ key: string; value: boolean }> +>( + ([_, value]) => F.either(value, G.isBoolean, G.isString), + ([key, value]) => + O.Some({ + key, + value: G.isString(value) ? value === "on" : (value as boolean), + }), + () => O.None, +); + // TODO: -// * handle virtual modules // * dead-code elimination by value of record `options` // * handle function as value when return promise binary value // * handle function as value when return binary value -const createFactory: CoreType.CreateFactory = (_options) => defaultFactory; +const createFactory: CoreType.CreateFactory = (data) => { + const state = new Set(); + const flagModule = ast.createFlagModule(data); + const addModuleIDToState = (importIn?: string) => { + importIn && state.add(importIn); + }; + const load = F.ifElse( + F.equals(data.options.moduleFlagName), + () => Promise.resolve({ code: flagModule.toString() }), + utils, + ); + + const flags = pipe( + data.flagConfig, + D.toPairs, + A.filterMap(valToBoolean), + F.toMutable, + ); + + // when user import `@flag` or `moduleFlagName` in codebase + // we MUST resolve it. + const resolveId: U["resolveId"] = (id, importIn, _option) => + F.ifElse( + id, + F.equals(data.options.moduleFlagName), + F.tap(F.identity, () => addModuleIDToState(importIn)), + () => undefined, + ); + + const transform: U["transform"] = (code: string, id: string) => + F.ifElse( + id, + F.both(state.has, () => N.gt(S.length(code), 0)), + (i) => + Promise.resolve(() => ({ + code: ast.transform(code, i, flags), + })).then((f) => f()), + () => undefined, + ); + + return D.merge(data.factory, { load, resolveId, transform }); +}; -const getOptionsR = (opt: CoreType.PluginOption) => - R.fromExecution(() => option.parse(opt)); +const getOptions: CoreType.GetOptions = (opt) => ({ + flagConfig: option.parse(opt), + factory: defaultFactory, + options: { + moduleFlagName: "@flag", + }, +}); export const unpluginFactory = flow( - getOptionsR, - R.map((options) => - pipe( - D.makeEmpty(), - D.set("options", options), - D.set("factory", createFactory(options)), - ), - ), - // R.map(factory), - // R.map(updateFactory), + F.identity, + (opt) => () => getOptions(opt), + R.fromExecution, + R.map((data) => D.set(data, "factory", createFactory(data))), R.mapWithDefault(defaultFactory, (data) => data.factory), ); diff --git a/packages/unplugin-flag/src/core/types.ts b/packages/unplugin-flag/src/core/types.ts index c66d147..f8c55da 100644 --- a/packages/unplugin-flag/src/core/types.ts +++ b/packages/unplugin-flag/src/core/types.ts @@ -5,7 +5,7 @@ export type PromiseLikeBinaryValue = PromiseLike; type UnaryFunction = () => T; -type FlagValue = +export type FlagValue = | BinaryValue | UnaryFunction | UnaryFunction; @@ -24,9 +24,14 @@ export type PluginOption = Record; export type UnpluginEnvironmentFactory = UnpluginFactory; -export type CreateFactory = (options: PluginOption) => UnpluginOptions; - export type Data = { - options: PluginOption; factory: UnpluginOptions; + flagConfig: PluginOption; + options: { + moduleFlagName: string; + }; }; + +export type EliminationFlag = { key: string; value: boolean }; +export type GetOptions = (options: PluginOption) => Data; +export type CreateFactory = (options: Data) => UnpluginOptions; diff --git a/packages/unplugin-flag/tsconfig.json b/packages/unplugin-flag/tsconfig.json index 5e4be26..eeda256 100644 --- a/packages/unplugin-flag/tsconfig.json +++ b/packages/unplugin-flag/tsconfig.json @@ -1,13 +1,24 @@ { - "compilerOptions": { - "target": "es2020", - "lib": ["esnext", "DOM"], - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "strict": true, - "strictNullChecks": true, - "esModuleInterop": true - }, - "exclude": ["dist", "eslint.config.js"] + "compilerOptions": { + "paths": { + "@utils": [ + "./../shared/utils.ts" + ] + }, + "target": "es2020", + "lib": [ + "esnext", + "DOM" + ], + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "esModuleInterop": true + }, + "exclude": [ + "dist", + "eslint.config.js" + ] } diff --git a/playground/index.html b/playground/index.html index 1ebec31..afe1670 100644 --- a/playground/index.html +++ b/playground/index.html @@ -11,6 +11,7 @@