Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ coverage

node_modules
gen
!packages/icons/lib/gen
dist
dist-ssr
*.local
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"workspaces": [
"packages/*",
"packages/react-sdk/dev/*",
"packages/icons/dev/*",
"packages/openfeature-browser-provider/example"
],
"scripts": {
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@reflag/cli",
"version": "1.0.2",
"version": "1.0.3",
"packageManager": "[email protected]",
"description": "CLI for Reflag service",
"main": "./dist/index.js",
Expand Down Expand Up @@ -31,8 +31,9 @@
"scripts": {
"build": "tsc && shx chmod +x dist/index.js",
"reflag": "yarn build && ./dist/index.js",
"test": "vitest -c vite.config.js",
"test:ci": "vitest run -c vite.config.js --reporter=default --reporter=junit --outputFile=junit.xml",
"test": "vitest run",
"test:watch": "vitest",
"test:ci": "yarn test --reporter=default --reporter=junit --outputFile=junit.xml",
"coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:ci": "eslint --output-file eslint-report.json --format json .",
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/test/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
JSONToType,
mergeTypeASTs,
quoteKey,
stringifyTypeAST,
toTypeAST,
TypeAST,
Expand Down Expand Up @@ -300,6 +301,37 @@ describe("JSON utilities", () => {
expect(stringifyTypeAST({ kind: "object", properties: [] })).toBe("{}");
});

it("should quote object keys with special characters", () => {
const ast: TypeAST = {
kind: "object",
properties: [
{
key: "my-key",
type: { kind: "primitive", type: "string" },
optional: false,
},
{
key: "my key",
type: { kind: "primitive", type: "string" },
optional: false,
},
{
key: "my.key",
type: { kind: "primitive", type: "string" },
optional: false,
},
{
key: "123key",
type: { kind: "primitive", type: "string" },
optional: false,
},
],
};

const expected = `{\n "my-key": string,\n "my key": string,\n "my.key": string,\n "123key": string\n}`;
expect(stringifyTypeAST(ast)).toBe(expected);
});

it("should stringify union types", () => {
const ast: TypeAST = {
kind: "union",
Expand Down Expand Up @@ -422,4 +454,14 @@ describe("JSON utilities", () => {
).toBe(expected);
});
});

describe("quoteKey", () => {
it("should quote keys with special characters", () => {
expect(quoteKey("my-key")).toBe('"my-key"');
expect(quoteKey("my key")).toBe('"my key"');
expect(quoteKey("my.key")).toBe('"my.key"');
expect(quoteKey("123key")).toBe('"123key"');
expect(quoteKey("key")).toBe("key");
});
});
});
4 changes: 2 additions & 2 deletions packages/cli/utils/gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { dirname, isAbsolute, join } from "node:path";

import { Flag, RemoteConfig } from "../services/flags.js";

import { JSONToType } from "./json.js";
import { JSONToType, quoteKey } from "./json.js";

export type GenFormat = "react" | "node";

Expand Down Expand Up @@ -118,7 +118,7 @@ ${flags
.map(({ key }) => {
const config = configDefs.get(key);
return indentLines(
`"${key}": ${config?.definition ? `{ config: { payload: ${config.name} } }` : "boolean"};`,
`${quoteKey(key)}: ${config?.definition ? `{ config: { payload: ${config.name} } }` : "boolean"};`,
4,
);
})
Expand Down
17 changes: 10 additions & 7 deletions packages/cli/utils/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,12 @@ export function stringifyTypeAST(ast: TypeAST, nestLevel = 0): string {
if (ast.properties.length === 0) return "{}";

return `{\n${ast.properties
.map(
({ key, optional, type }) =>
`${nextIndent}${key}${optional ? "?" : ""}: ${stringifyTypeAST(
type,
nestLevel + 1,
)}`,
)
.map(({ key, optional, type }) => {
return `${nextIndent}${quoteKey(key)}${optional ? "?" : ""}: ${stringifyTypeAST(
type,
nestLevel + 1,
)}`;
})
.join(",\n")}\n${indent}}`;

case "union":
Expand All @@ -198,6 +197,10 @@ export function stringifyTypeAST(ast: TypeAST, nestLevel = 0): string {
}
}

export function quoteKey(key: string): string {
return /[^a-zA-Z0-9_]/.test(key) || /^[0-9]/.test(key) ? `"${key}"` : key;
}

// Convert JSON array to TypeScript type
export function JSONToType(json: JSONPrimitive[]): string | null {
if (!json.length) return null;
Expand Down
169 changes: 169 additions & 0 deletions packages/icons/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# @reflag/icons

A universal icon library compatible with both **React** and **Preact**.

## Installation

```bash
npm install @reflag/icons
# or
yarn add @reflag/icons
# or
pnpm add @reflag/icons
```

## Peer Dependencies

This library requires either React or Preact:

- **React**: `^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0`
- **Preact**: `^10.0.0`

You only need to install one of them based on your project's framework.

## Usage

### Basic Usage (React)

```tsx
import { Icon } from "@reflag/icons";
import Icons from "@reflag/icons/icons.svg?raw";

function App() {
return (
<>
{/* Load the SVG sprite (only once in your app) */}
<div dangerouslySetInnerHTML={{ __html: Icons }} />

{/* Use the Icon component */}
<Icon name="Project" size={24} color="#ff0000" />
</>
);
}
```

### Basic Usage (Preact)

For Preact projects, you need to configure your build tool to alias React to Preact:

#### Using Vite

```js
// vite.config.js
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";

export default defineConfig({
plugins: [preact()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
},
},
});
```

Then use the same component:

```tsx
import { Icon } from "@reflag/icons";
import Icons from "@reflag/icons/icons.svg?raw";

export function App() {
return (
<>
{/* Load the SVG sprite (only once in your app) */}
<div dangerouslySetInnerHTML={{ __html: Icons }} />

{/* Use the Icon component */}
<Icon name="Project" size={24} color="#ff0000" />
</>
);
}
```

## API

### `Icon` Component

#### Props

| Prop | Type | Default | Description |
| ---------- | ------------------ | ---------------- | ------------------------------- |
| `name` | `IconKey` | _required_ | The name of the icon to display |
| `size` | `number \| string` | `16` | Width and height of the icon |
| `color` | `string` | `"currentColor"` | Color of the icon |
| `...props` | `SVGAttributes` | - | Any other valid SVG attributes |

#### Example with all props

```tsx
<Icon
name="Project"
size={32}
color="#3b82f6"
className="my-icon"
style={{ margin: "10px" }}
onClick={() => console.log("clicked")}
/>
```

## TypeScript

The library is fully typed. The `IconKey` type provides autocomplete for all available icon names.

```tsx
import type { IconProps, IconKey } from "@reflag/icons";

const iconName: IconKey = "Project"; // Autocomplete available!

function MyIcon(props: IconProps) {
return <Icon {...props} />;
}
```

## How It Works

This library uses React's JSX runtime but is compatible with both React and Preact:

- **For React projects**: The library works out of the box
- **For Preact projects**: Use Preact's compatibility layer by aliasing `react` to `preact/compat`

The library exports React types but Preact's compatibility layer ensures they work seamlessly in Preact applications.

## Available Icons

The library includes icons synced from Figma. To see all available icons, check the type definitions or run the dev examples:

```bash
# React example
cd dev/react && npm install && npm run dev

# Preact example
cd dev/preact && npm install && npm run dev
```

## Development

### Building the library

```bash
yarn build
```

### Syncing icons from Figma

```bash
yarn sync
```

### Linting

```bash
yarn lint
```

## License

MIT
24 changes: 24 additions & 0 deletions packages/icons/dev/preact/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
13 changes: 13 additions & 0 deletions packages/icons/dev/preact/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>preact</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions packages/icons/dev/preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "preact",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@reflag/icons": "workspace:*",
"preact": "^10.27.2"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"@types/node": "^24.6.0",
"typescript": "~5.9.3",
"vite": "^7.1.7"
}
}
1 change: 1 addition & 0 deletions packages/icons/dev/preact/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading