From d41cab7a6e4c2e47f9f380b996ec9243f3042844 Mon Sep 17 00:00:00 2001 From: cohitre Date: Tue, 13 Feb 2024 08:00:43 -0800 Subject: [PATCH] Adding basic Block and configuration builders (#1) --- .eslintignore | 1 + .eslintrc.json | 2 +- .github/workflows/ci.yaml | 24 ++++++++++ .gitignore | 2 + .npmignore | 9 ++++ jest.config.ts | 6 +-- package.json | 6 ++- src/builders/index.tsx | 41 ++++++++++++++++ src/index.tsx | 1 + .../builder/__snapshots__/index.spec.tsx.snap | 9 ++++ tests/builder/index.spec.tsx | 48 +++++++++++++++++++ tsconfig.build.json | 4 ++ tsconfig.json | 13 ++--- 13 files changed, 150 insertions(+), 16 deletions(-) create mode 100644 .eslintignore create mode 100644 .github/workflows/ci.yaml create mode 100644 .npmignore create mode 100644 src/builders/index.tsx create mode 100644 src/index.tsx create mode 100644 tests/builder/__snapshots__/index.spec.tsx.snap create mode 100644 tests/builder/index.spec.tsx create mode 100644 tsconfig.build.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 2c6429e..0a1dc8a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": 12, + "ecmaVersion": 2015, "project": "./tsconfig.json", "sourceType": "module" }, diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c3a5342 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,24 @@ +name: CI - Code styles, unit tests +on: + push: + branches: [main] + pull_request: + branches: + - main +concurrency: + group: ${{ github.head_ref }}-codestyles + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: 'npm' + - run: npm ci + - run: npx eslint . + - run: npx prettier . --check + - run: npm test diff --git a/.gitignore b/.gitignore index c072118..1d8411f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ node_modules npm-debug.log* yarn-debug.log* yarn-error.log* + +dist \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..564b640 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +.editorconfig +.envrc +.eslintignore +.eslintrc.json +.prettierrc +jest.config.ts +src +tests +tsconfig.json \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index f7a6a00..04907e5 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,8 +1,8 @@ -import type { Config } from "jest"; +import type { Config } from 'jest'; const config: Config = { - preset: "ts-jest", - testEnvironment: "jsdom", + preset: 'ts-jest', + testEnvironment: 'jsdom', }; export default config; diff --git a/package.json b/package.json index 01b9650..0b19608 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ "description": "Tools to render waypoint-style documents.", "main": "dist/index.js", "types": "dist/index.d.ts", + "target": "ES2022", "files": [ - "lib" + "dist" ], "scripts": { - "prepublish": "./node_modules/.bin/tsc", + "build": "npx tsc", + "prepublish": "npx tsc --project tsconfig.build.json", "test": "npx jest" }, "author": "carlos@usewaypoint.com", diff --git a/src/builders/index.tsx b/src/builders/index.tsx new file mode 100644 index 0000000..957e9a3 --- /dev/null +++ b/src/builders/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { z } from 'zod'; + +export type DocumentBlocksDictionary = { + [K in keyof T]: { + schema: T[K]; + Component: (props: z.infer) => React.ReactNode; + }; +}; + +export function buildBlockConfigurationSchema( + blocks: DocumentBlocksDictionary +) { + type BaseBlockComponentProps = { + id: string; + type: TType; + data: z.infer; + }; + + const blockObjects = Object.keys(blocks).map((type: keyof T) => + z.object({ + id: z.string(), + type: z.literal(type), + data: blocks[type].schema, + }) + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return z.discriminatedUnion('type', blockObjects as any).transform((v) => v as BaseBlockComponentProps); +} + +export function buildBlockComponent(blocks: DocumentBlocksDictionary) { + type BaseBlockComponentProps = { + type: TType; + data: z.infer; + }; + + return function BlockComponent({ type, data }: BaseBlockComponentProps): React.ReactNode { + return React.createElement(blocks[type].Component, data); + }; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..a00a45c --- /dev/null +++ b/src/index.tsx @@ -0,0 +1 @@ +export * from './builders'; diff --git a/tests/builder/__snapshots__/index.spec.tsx.snap b/tests/builder/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000..9f714f0 --- /dev/null +++ b/tests/builder/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`builders buildBlockComponent renders the specified component 1`] = ` + +
+ TEST TEXT! +
+
+`; diff --git a/tests/builder/index.spec.tsx b/tests/builder/index.spec.tsx new file mode 100644 index 0000000..a648d42 --- /dev/null +++ b/tests/builder/index.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { z } from 'zod'; + +import { render } from '@testing-library/react'; + +import { buildBlockComponent, buildBlockConfigurationSchema } from '../../src/builders'; + +describe('builders', () => { + describe('buildBlockComponent', () => { + it('renders the specified component', () => { + const BlockComponent = buildBlockComponent({ + SampleBlock: { + schema: z.object({ text: z.string() }), + Component: ({ text }) =>
{text.toUpperCase()}
, + }, + }); + expect( + render().asFragment() + ).toMatchSnapshot(); + }); + }); + + describe('buildBlockConfigurationSchema', () => { + it('adds an id, data, and type to the provided schema', () => { + const blockConfigurationSchema = buildBlockConfigurationSchema({ + SampleBlock: { + schema: z.object({ text: z.string() }), + Component: ({ text }) =>
{text.toUpperCase()}
, + }, + }); + + const sampleValidData = { + id: 'my id', + type: 'SampleBlock', + data: { text: 'Test text!' }, + }; + const parsedData = blockConfigurationSchema.safeParse(sampleValidData); + expect(parsedData).toEqual({ + success: true, + data: { + id: 'my id', + type: 'SampleBlock', + data: { text: 'Test text!' }, + }, + }); + }); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..9dabb8e --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["tests/**/*.spec.ts", "tests/**/*.spec.tsx", "jest.config.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index be7c121..48113e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2015", "module": "esnext", "lib": [], "moduleResolution": "node", @@ -8,20 +8,13 @@ "outDir": "dist", "strict": true, "sourceMap": true, - "noImplicitAny": true, - "forceConsistentCasingInFileNames": true, "esModuleInterop": true, "declarationMap": true, "declaration": true, - "noImplicitThis": true, - "alwaysStrict": true, "noUnusedLocals": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "inlineSources": true, - "strictPropertyInitialization": true, - "strictNullChecks": true + "noFallthroughCasesInSwitch": true }, - "include": ["./jest.config.ts", "src/**/*.ts", "tests/**/*.ts", "src/**/*.tsx", "tests/**/*.tsx"], + "include": ["jest.config.ts", "src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"], "exclude": ["node_modules", "dist"] }