From 141dbaf6b80b647e5061517d11849844e3e16f80 Mon Sep 17 00:00:00 2001 From: Anton Evzhakov Date: Tue, 3 Oct 2023 19:02:36 +0300 Subject: [PATCH] chore: example of object syntax (#5) --- examples/object-syntax/.eslintrc.js | 3 + examples/object-syntax/babel.config.js | 3 + examples/object-syntax/jest.config.js | 14 ++ examples/object-syntax/package.json | 64 +++++++ .../object-syntax/processors/makeStyles.js | 5 + .../src/__tests__/getTagProcessor.test.ts | 168 ++++++++++++++++++ examples/object-syntax/src/index.ts | 1 + examples/object-syntax/src/makeStyles.ts | 6 + .../src/processors/makeStyles.ts | 114 ++++++++++++ examples/object-syntax/tsconfig.eslint.json | 4 + examples/object-syntax/tsconfig.json | 13 ++ examples/object-syntax/tsconfig.spec.json | 5 + .../src/__tests__/getTagProcessor.test.ts | 8 +- pnpm-lock.yaml | 76 +++++++- 14 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 examples/object-syntax/.eslintrc.js create mode 100644 examples/object-syntax/babel.config.js create mode 100644 examples/object-syntax/jest.config.js create mode 100644 examples/object-syntax/package.json create mode 100644 examples/object-syntax/processors/makeStyles.js create mode 100644 examples/object-syntax/src/__tests__/getTagProcessor.test.ts create mode 100644 examples/object-syntax/src/index.ts create mode 100644 examples/object-syntax/src/makeStyles.ts create mode 100644 examples/object-syntax/src/processors/makeStyles.ts create mode 100644 examples/object-syntax/tsconfig.eslint.json create mode 100644 examples/object-syntax/tsconfig.json create mode 100644 examples/object-syntax/tsconfig.spec.json diff --git a/examples/object-syntax/.eslintrc.js b/examples/object-syntax/.eslintrc.js new file mode 100644 index 00000000..1ad149b8 --- /dev/null +++ b/examples/object-syntax/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@wyw-in-js/eslint-config/library'], +}; diff --git a/examples/object-syntax/babel.config.js b/examples/object-syntax/babel.config.js new file mode 100644 index 00000000..dda52988 --- /dev/null +++ b/examples/object-syntax/babel.config.js @@ -0,0 +1,3 @@ +const config = require('@wyw-in-js/babel-config'); + +module.exports = config; diff --git a/examples/object-syntax/jest.config.js b/examples/object-syntax/jest.config.js new file mode 100644 index 00000000..8f000f23 --- /dev/null +++ b/examples/object-syntax/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.test.ts'], + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.spec.json', + }, + ], + }, +}; diff --git a/examples/object-syntax/package.json b/examples/object-syntax/package.json new file mode 100644 index 00000000..425a99dc --- /dev/null +++ b/examples/object-syntax/package.json @@ -0,0 +1,64 @@ +{ + "name": "@wyw-in-js/object-syntax", + "private": true, + "version": "0.0.1", + "dependencies": { + "@griffel/core": "1.5.0", + "@wyw-in-js/processor-utils": "workspace:*" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@types/babel__core": "^7.20.2", + "@types/babel__traverse": "^7.20.2", + "@types/node": "^16.18.55", + "@wyw-in-js/babel-config": "workspace:*", + "@wyw-in-js/eslint-config": "workspace:*", + "@wyw-in-js/shared": "workspace:*", + "@wyw-in-js/transform": "workspace:*", + "@wyw-in-js/ts-config": "workspace:*", + "dedent": "^1.5.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./types/index.d.ts", + "import": "./esm/index.js", + "default": "./lib/index.js" + }, + "./*": { + "types": "./types/*.d.ts", + "import": "./esm/*.js", + "default": "./lib/*.js" + } + }, + "files": [ + "esm/", + "lib/", + "processors/", + "types/" + ], + "license": "MIT", + "linaria": { + "tags": { + "makeStyles": "./lib/processors/makeStyles.js" + } + }, + "main": "lib/index.js", + "module": "esm/index.js", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:types": "tsc", + "lint": "eslint --ext .js,.ts .", + "test": "jest --config ./jest.config.js --rootDir src" + }, + "types": "types/index.d.ts" +} diff --git a/examples/object-syntax/processors/makeStyles.js b/examples/object-syntax/processors/makeStyles.js new file mode 100644 index 00000000..33a4817a --- /dev/null +++ b/examples/object-syntax/processors/makeStyles.js @@ -0,0 +1,5 @@ +Object.defineProperty(exports, '__esModule', { + value: true, +}); + +exports.default = require('../lib/processors/makeStyles').default; diff --git a/examples/object-syntax/src/__tests__/getTagProcessor.test.ts b/examples/object-syntax/src/__tests__/getTagProcessor.test.ts new file mode 100644 index 00000000..2d2765be --- /dev/null +++ b/examples/object-syntax/src/__tests__/getTagProcessor.test.ts @@ -0,0 +1,168 @@ +import { join } from 'path'; + +import { parseSync } from '@babel/core'; +import traverse from '@babel/traverse'; +import dedent from 'dedent'; + +import type { BaseProcessor } from '@wyw-in-js/processor-utils'; +import { getTagProcessor } from '@wyw-in-js/transform'; + +interface IRunOptions { + ts?: boolean; +} + +const run = (code: string, options: IRunOptions = {}): BaseProcessor | null => { + const opts = { + filename: join(__dirname, options.ts ? 'test.ts' : 'test.js'), + root: '.', + code: true, + ast: true, + presets: options.ts ? ['@babel/preset-typescript'] : [], + }; + const rootNode = parseSync(code, opts)!; + let result: BaseProcessor | null = null; + traverse(rootNode, { + Identifier(path) { + const processor = getTagProcessor(path, opts, { + displayName: true, + evaluate: true, + }); + + if (processor) { + result = processor; + } + }, + }); + + return result; +}; + +function tagToString(processor: BaseProcessor | null): string | undefined { + if (!processor) return undefined; + return processor.toString(); +} + +describe('getTagProcessor', () => { + it('should find correct import', () => { + const result = run( + dedent` + import { makeStyles } from "@wyw-in-js/object-syntax"; + + export const Square = makeStyles({}); + ` + ); + + expect(tagToString(result)).toBe('makeStyles(…)'); + expect(result?.tagSource).toEqual({ + imported: 'makeStyles', + source: '@wyw-in-js/object-syntax', + }); + }); + + it('renamed({})', () => { + const result = run( + dedent` + import { makeStyles as renamed } from "@wyw-in-js/object-syntax"; + + export const Square = renamed({}); + ` + ); + + expect(tagToString(result)).toBe('renamed(…)'); + expect(result?.tagSource).toEqual({ + imported: 'makeStyles', + source: '@wyw-in-js/object-syntax', + }); + }); + + it('(0, objectSyntax.makeStyles)()', () => { + const result = run( + dedent` + const objectSyntax = require("@wyw-in-js/object-syntax"); + + export const Square = (0, objectSyntax.makeStyles)({}); + ` + ); + + expect(tagToString(result)).toBe('objectSyntax.makeStyles(…)'); + expect(result?.tagSource).toEqual({ + imported: 'makeStyles', + source: '@wyw-in-js/object-syntax', + }); + }); + + it('imported from file', () => { + const result = run( + dedent` + import { makeStyles } from '../makeStyles'; + + + export const square = makeStyles({}); + ` + ); + + expect(tagToString(result)).toBe('makeStyles(…)'); + expect(result?.tagSource).toEqual({ + imported: 'makeStyles', + source: '../makeStyles', + }); + }); + + it('require and access with prop', () => { + const result = run( + dedent` + const renamed = require('@wyw-in-js/object-syntax').makeStyles; + export const Square = renamed({}); + ` + ); + + expect(tagToString(result)).toBe('renamed(…)'); + }); + + it('require and destructing', () => { + const result = run( + dedent` + const { makeStyles } = require('@wyw-in-js/object-syntax'); + export const Square = makeStyles({}); + ` + ); + + expect(tagToString(result)).toBe('makeStyles(…)'); + }); + + describe('invalid usage', () => { + it('makeStyles``', () => { + const runner = () => + run( + dedent`import { makeStyles } from "@wyw-in-js/object-syntax"; export const square = makeStyles\`\`;` + ); + + expect(runner).toThrow('Invalid usage of `makeStyles` function'); + }); + + it('makeStyles.div``', () => { + const runner = () => + run( + dedent`import { makeStyles } from "@wyw-in-js/object-syntax"; export const square = makeStyles.div\`\`;` + ); + + expect(runner).toThrow('Invalid usage of `makeStyles` function'); + }); + + it('makeStyles("div")``', () => { + const runner = () => + run( + dedent`import { makeStyles } from "@wyw-in-js/object-syntax"; export const square = makeStyles("div")\`\`;` + ); + + expect(runner).toThrow('Invalid usage of `makeStyles` function'); + }); + + it('do not throw if css is not a call', () => { + const runner = () => + run(dedent`export { makeStyles } from "@wyw-in-js/object-syntax";`); + + expect(runner).not.toThrow(); + }); + }); +}); diff --git a/examples/object-syntax/src/index.ts b/examples/object-syntax/src/index.ts new file mode 100644 index 00000000..90e42508 --- /dev/null +++ b/examples/object-syntax/src/index.ts @@ -0,0 +1 @@ +export { makeStyles } from './makeStyles'; diff --git a/examples/object-syntax/src/makeStyles.ts b/examples/object-syntax/src/makeStyles.ts new file mode 100644 index 00000000..0d8a6e2d --- /dev/null +++ b/examples/object-syntax/src/makeStyles.ts @@ -0,0 +1,6 @@ +export function makeStyles( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stylesBySlots: Record +): () => Record { + throw new Error('Cannot be called in runtime'); +} diff --git a/examples/object-syntax/src/processors/makeStyles.ts b/examples/object-syntax/src/processors/makeStyles.ts new file mode 100644 index 00000000..4627171f --- /dev/null +++ b/examples/object-syntax/src/processors/makeStyles.ts @@ -0,0 +1,114 @@ +/* eslint-disable class-methods-use-this */ +import type { Expression } from '@babel/types'; +import { resolveStyleRulesForSlots } from '@griffel/core'; +import type { + StylesBySlots, + CSSClassesMapBySlot, + CSSRulesByBucket, +} from '@griffel/core/types'; + +import type { + ValueCache, + Params, + TailProcessorParams, +} from '@wyw-in-js/processor-utils'; +import { BaseProcessor, validateParams } from '@wyw-in-js/processor-utils'; + +export default class MakeStylesProcessor extends BaseProcessor { + #cssClassMap: CSSClassesMapBySlot | undefined; + + #cssRulesByBucket: CSSRulesByBucket | undefined; + + readonly #slotsExpName: string | number | boolean | null; + + public constructor(params: Params, ...args: TailProcessorParams) { + validateParams( + params, + ['callee', 'call'], + 'Invalid usage of `makeStyles` function' + ); + const [callee, callParam] = params; + + super([callee], ...args); + + const { ex } = callParam[1]; + if (ex.type === 'Identifier') { + this.dependencies.push(callParam[1]); + this.#slotsExpName = ex.name; + } else if (ex.type === 'NullLiteral') { + this.#slotsExpName = null; + } else { + this.#slotsExpName = ex.value; + } + } + + public override get asSelector(): string { + throw new Error('The result of makeStyles cannot be used as a selector.'); + } + + public override get value(): Expression { + return this.astService.nullLiteral(); + } + + public override build(valueCache: ValueCache) { + const slots = valueCache.get(this.#slotsExpName) as StylesBySlots; + [this.#cssClassMap, this.#cssRulesByBucket] = + resolveStyleRulesForSlots(slots); + } + + public override doEvaltimeReplacement(): void { + this.replacer(this.value, false); + } + + public override doRuntimeReplacement(): void { + if (!this.#cssClassMap || !this.#cssRulesByBucket) { + throw new Error( + 'Styles are not extracted yet. Please call `build` first.' + ); + } + + const t = this.astService; + + const importedStyles = t.addNamedImport('__styles', '@griffel/react'); + + const cssClassMap = t.objectExpression( + Object.entries(this.#cssClassMap).map(([slot, classesMap]) => { + return t.objectProperty( + t.identifier(slot), + t.objectExpression( + Object.entries(classesMap).map(([className, classValue]) => + t.objectProperty( + t.identifier(className), + Array.isArray(classValue) + ? t.arrayExpression(classValue.map((i) => t.stringLiteral(i))) + : t.stringLiteral(classValue) + ) + ) + ) + ); + }) + ); + + const cssRulesByBucket = t.objectExpression( + Object.entries(this.#cssRulesByBucket).map(([bucket, rules]) => { + return t.objectProperty( + t.identifier(bucket), + t.arrayExpression( + // FIXME: rule can be [string, Record] + rules.map((rule) => t.stringLiteral(rule as string)) + ) + ); + }) + ); + + const stylesCall = t.callExpression(importedStyles, [ + cssClassMap, + cssRulesByBucket, + ]); + this.replacer(stylesCall, true); + } + + public override toString(): string { + return `${super.toString()}(…)`; + } +} diff --git a/examples/object-syntax/tsconfig.eslint.json b/examples/object-syntax/tsconfig.eslint.json new file mode 100644 index 00000000..a8d4317b --- /dev/null +++ b/examples/object-syntax/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": [] +} diff --git a/examples/object-syntax/tsconfig.json b/examples/object-syntax/tsconfig.json new file mode 100644 index 00000000..79f84eaa --- /dev/null +++ b/examples/object-syntax/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@wyw-in-js/ts-config/node.json", + "include": ["src/**/*"], + "exclude": ["src/__tests__/*", "src/**/__tests__/*"], + "compileOnSave": true, + "compilerOptions": { + "baseUrl": "src/", + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "types", + "rootDir": "src/" + } +} diff --git a/examples/object-syntax/tsconfig.spec.json b/examples/object-syntax/tsconfig.spec.json new file mode 100644 index 00000000..f79471af --- /dev/null +++ b/examples/object-syntax/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": [] +} diff --git a/examples/template-tag-syntax/src/__tests__/getTagProcessor.test.ts b/examples/template-tag-syntax/src/__tests__/getTagProcessor.test.ts index 49e7a291..0fd9fc07 100644 --- a/examples/template-tag-syntax/src/__tests__/getTagProcessor.test.ts +++ b/examples/template-tag-syntax/src/__tests__/getTagProcessor.test.ts @@ -59,16 +59,16 @@ describe('getTagProcessor', () => { }); }); - it('renamedCss``', () => { + it('renamed``', () => { const result = run( dedent` - import { css as renamedCss } from "@wyw-in-js/template-tag-syntax"; + import { css as renamed } from "@wyw-in-js/template-tag-syntax"; - export const Square = renamedCss\`\`; + export const Square = renamed\`\`; ` ); - expect(tagToString(result)).toBe('renamedCss`…`'); + expect(tagToString(result)).toBe('renamed`…`'); expect(result?.tagSource).toEqual({ imported: 'css', source: '@wyw-in-js/template-tag-syntax', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ed262a9..d369fbf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,52 @@ importers: configs/ts: {} + examples/object-syntax: + dependencies: + '@griffel/core': + specifier: 1.5.0 + version: 1.5.0 + '@wyw-in-js/processor-utils': + specifier: workspace:* + version: link:../../packages/processor-utils + devDependencies: + '@babel/core': + specifier: ^7.23.0 + version: 7.23.0 + '@babel/traverse': + specifier: ^7.23.0 + version: 7.23.0 + '@babel/types': + specifier: ^7.23.0 + version: 7.23.0 + '@types/babel__core': + specifier: ^7.20.2 + version: 7.20.2 + '@types/babel__traverse': + specifier: ^7.20.2 + version: 7.20.2 + '@types/node': + specifier: ^16.18.55 + version: 16.18.55 + '@wyw-in-js/babel-config': + specifier: workspace:* + version: link:../../configs/babel + '@wyw-in-js/eslint-config': + specifier: workspace:* + version: link:../../configs/eslint + '@wyw-in-js/shared': + specifier: workspace:* + version: link:../../packages/shared + '@wyw-in-js/transform': + specifier: workspace:* + version: link:../../packages/transform + '@wyw-in-js/ts-config': + specifier: workspace:* + version: link:../../configs/ts + dedent: + specifier: ^1.5.1 + version: 1.5.1 + examples/template-tag-syntax: dependencies: '@wyw-in-js/processor-utils': @@ -1616,7 +1662,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: true /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} @@ -1872,6 +1917,10 @@ packages: fast-check: 3.13.1 dev: true + /@emotion/hash@0.8.0: + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + dev: false + /@esbuild/android-arm@0.15.18: resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} engines: {node: '>=12'} @@ -1927,6 +1976,16 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@griffel/core@1.5.0: + resolution: {integrity: sha512-NC0J3k4qxQq7fLicrlbbPIC85Gw7LgTUm0c9SWGRv7dsj1tXE6y2+eHAhJwA5230KkUvACPgwJRCaBikFxv+4Q==} + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.1.2 + rtl-css-js: 1.16.1 + stylis: 4.3.0 + tslib: 2.6.2 + dev: false + /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} @@ -3494,6 +3553,10 @@ packages: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} dev: false + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + dev: false + /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} dev: true @@ -6581,7 +6644,6 @@ packages: /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: true /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} @@ -6685,6 +6747,12 @@ packages: glob: 7.2.3 dev: true + /rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + dependencies: + '@babel/runtime': 7.23.1 + dev: false + /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -7038,6 +7106,10 @@ packages: resolution: {integrity: sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==} dev: false + /stylis@4.3.0: + resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'}