├── .gitignore ├── .npmrc ├── .prettierrc.mjs ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── screenshot_autocomplete_space.png ├── screenshot_autocomplete_theme.png ├── screenshot_autocomplete_translucent.png ├── screenshot_quickinfo_color.png ├── screenshot_quickinfo_shorthand.png └── screenshot_quickinfo_space.png ├── package.json ├── pnpm-lock.yaml ├── src ├── getCompletionDetails.ts ├── getCompletions.ts ├── getQuickInfo.ts ├── getTokens.ts ├── hooks.ts ├── index.ts ├── mapPropToToken.ts ├── metadata.ts ├── readConfig.ts ├── readOptions.ts ├── safeInsertDocs.ts ├── types.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | .DS_Store 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # cra 65 | .cache 66 | 67 | # tdsx 68 | dist 69 | 70 | .yarn/cache 71 | .yarn/unplugged 72 | .yarn/build-state.yml 73 | .yarn/install-state.gz 74 | out -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix='' 2 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig} */ 2 | export default { 3 | arrowParens: 'always', 4 | printWidth: 80, 5 | singleQuote: true, 6 | jsxSingleQuote: false, 7 | semi: true, 8 | trailingComma: 'es5', 9 | tabWidth: 2, 10 | plugins: ['@ianvs/prettier-plugin-sort-imports'], 11 | // @ianvs/prettier-plugin-sort-imports settings: 12 | importOrder: [ 13 | '', 14 | '', 15 | '', 16 | '~/(.*)$', 17 | '^\\.\\.?/(.*)$', 18 | ], 19 | importOrderTypeScriptVersion: '5.1.6', 20 | }; 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | // See: https://github.com/microsoft/TypeScript/wiki/Debugging-Language-Service-in-VS-Code 9 | "type": "node", 10 | "request": "attach", 11 | "name": "Attach to VS Code TS Server via Port", 12 | "processId": "${command:PickProcess}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "eslint.enable": true, 5 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 6 | "typescript.tsdk": "./node_modules/typescript/lib" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.6.0 (2023-10-15) 2 | 3 | ##### New Features 4 | 5 | - add real prop translations for shorthands (thanks @kaceycleveland) ([ee56aaaa](https://github.com/nderscore/tamagui-typescript-plugin/commit/ee56aaaa586218daea62ebbc79496bfe5d1f1a13)) 6 | 7 | #### 0.5.4 (2023-08-27) 8 | 9 | ##### Bug Fixes 10 | 11 | - avoid calling getProgram during initialization ([adb57ded](https://github.com/nderscore/tamagui-typescript-plugin/commit/adb57ded9017953db1ca7f459d9eb1a67b2e3ab0)) 12 | 13 | ##### Refactors 14 | 15 | - improved prop/value extraction and tamagui component detection ([f78ef9ea](https://github.com/nderscore/tamagui-typescript-plugin/commit/f78ef9ea191e7c6356d29bb35bbe587abce0db6d)) 16 | 17 | #### 0.5.3 (2023-08-24) 18 | 19 | ##### Bug Fixes 20 | 21 | - use original quick info result if no tamagui token found ([e8b38534](https://github.com/nderscore/tamagui-typescript-plugin/commit/e8b3853485f34bac6de2cff90c0da95329909eb4)) 22 | 23 | #### 0.5.2 (2023-08-22) 24 | 25 | ##### Bug Fixes 26 | 27 | - prop type detection in nested styles (psuedostates/media) ([4bd02e1c](https://github.com/nderscore/tamagui-typescript-plugin/commit/4bd02e1c7048984d20723c897dc949df240df487)) 28 | 29 | #### 0.5.1 (2023-08-20) 30 | 31 | ##### Bug Fixes 32 | 33 | - hide true in all scales when showTrueTokens=false ([68a8b073](https://github.com/nderscore/tamagui-typescript-plugin/commit/68a8b073d4e9991982947b8aa1ccd3679ca34ad5)) 34 | 35 | ### 0.5.0 (2023-08-19) 36 | 37 | ##### New Features 38 | 39 | - add option for custom autocomplete filters ([282c2149](https://github.com/nderscore/tamagui-typescript-plugin/commit/282c21491aded667b23ec9764568d7c2ea830c94)) 40 | - add option showTrueTokens, allows filtering true and -true space/size tokens from autocomplete ([50ff939f](https://github.com/nderscore/tamagui-typescript-plugin/commit/50ff939fea227e20ede2315222a618bd9a2e96ed)) 41 | - add option showColorTokens, allows filtering color tokens from autocomplete results ([3026cc33](https://github.com/nderscore/tamagui-typescript-plugin/commit/3026cc3310017b85eb949129e63d18f1240315fe)) 42 | - add configuration option colorTileSize, default is now 18 ([da977ad6](https://github.com/nderscore/tamagui-typescript-plugin/commit/da977ad6c0053dbcb932d678482573ff8313e00b)) 43 | - replace rounded square tiles with squircles ([97d558f4](https://github.com/nderscore/tamagui-typescript-plugin/commit/97d558f4c7e9e660c031040369330f8b09b7912e)) 44 | 45 | ### 0.4.0 (2023-08-18) 46 | 47 | ##### New Features 48 | 49 | - group themes by parent in previews ([d6c8883d](https://github.com/nderscore/tamagui-typescript-plugin/commit/d6c8883d971a50fcb9f556b8b2d56573b721bedf)) 50 | - sort default theme first in previews ([d9a3d9ea](https://github.com/nderscore/tamagui-typescript-plugin/commit/d9a3d9eaba83978d3c6c1a03d90b9613846ffa1d)) 51 | 52 | ##### Bug Fixes 53 | 54 | - theme token sorting ([d959172d](https://github.com/nderscore/tamagui-typescript-plugin/commit/d959172dc6ea510b1fc77dc275df53cec082be4f)) 55 | - highlight only the token value string on hover ([4be5ee40](https://github.com/nderscore/tamagui-typescript-plugin/commit/4be5ee40a3111836f590fd15482dd1c3cfa775f3)) 56 | - prevent completion error if default theme is missing ([24038bdf](https://github.com/nderscore/tamagui-typescript-plugin/commit/24038bdf937090afc4f3e566c35bdc12c516fb4f)) 57 | 58 | ##### Performance Improvements 59 | 60 | - improve AST walking performance ([0241c27b](https://github.com/nderscore/tamagui-typescript-plugin/commit/0241c27b6e9bdf07a369c22c41fa012cbbfd7c90)) 61 | 62 | #### 0.3.1 (2023-08-16) 63 | 64 | ##### Bug Fixes 65 | 66 | - fix quick info on JSX props ([2404d22f](https://github.com/nderscore/tamagui-typescript-plugin/commit/2404d22ff7e3c85911b77d0f29fa0dbbf20f9429)) 67 | 68 | ### 0.3.0 (2023-08-16) 69 | 70 | ##### New Features 71 | 72 | - token previews on hover (quick info) ([afca5d9d](https://github.com/nderscore/tamagui-typescript-plugin/commit/afca5d9d17bcd9b4d71216fb471ca75cf5da8834)) 73 | 74 | ##### Bug Fixes 75 | 76 | - tighten token sorting rules and specific token detection ([df293dc1](https://github.com/nderscore/tamagui-typescript-plugin/commit/df293dc19bdaed5fabe4e23b73cdfbce47b0edad)) 77 | 78 | ### 0.2.0 (2023-08-15) 79 | 80 | ##### New Features 81 | 82 | - smart sorting of all tokens ([334cf22d](https://github.com/nderscore/tamagui-typescript-plugin/commit/334cf22d40cf1475a9240e517fdb8a84bb553bec)) 83 | - sort size/space tokens in numeric order, sort negative tokens last ([ccff2671](https://github.com/nderscore/tamagui-typescript-plugin/commit/ccff26712783a0dbddbd16861291b63b9f3f2072)) 84 | - color icon for theme and color tokens ([91b526bb](https://github.com/nderscore/tamagui-typescript-plugin/commit/91b526bb27da7d5c03f4fc9b5f9cef334669b1b7)) 85 | 86 | ##### Bug Fixes 87 | 88 | - improve support for specific tokens ([71ed0d26](https://github.com/nderscore/tamagui-typescript-plugin/commit/71ed0d26675ed45d3dae958ba6cc110547c26f08)) 89 | 90 | ### 0.1.0 (2023-08-13) 91 | 92 | Initial release. 93 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Set up local dev environment 4 | 5 | 1. Use `yalc` / `npm link` / etc to link this project folder to your tamagui project. 6 | 7 | 1. Add plugin config to `tsconfig.json` (see [README](./README.md)) 8 | 9 | ## Development workflow 10 | 11 | 1. Run `pnpm build` in plugin workspace 12 | 13 | 1. Refresh your tamagui project vscode window / TS server 14 | 15 | 1. Trigger an autocomplete to appear 16 | 17 | 1. Check logs: 18 | 19 | - Open VSCode `Command Palette` -> `Open TS Server Logs...` 20 | 21 | - `ctrl/cmd + f` -> `TSTamagui::` 22 | 23 | 1. Start over from build step after making changes to the plugin code. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 _nderscore 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nderscore/tamagui-typescript-plugin 2 | 3 | Typescript Language Server Plugin for [Tamagui](https://tamagui.dev). 4 | 5 | - [Features](#features) 6 | - [Screenshots](#screenshots) 7 | - [Changelog](#changelog) 8 | - [Setup](#setup) 9 | - [Contributing](#contributing) 10 | 11 | ## Features 12 | 13 | - Show Tamagui theme/token values in TypeScript autocomplete suggestions 14 | 15 | - Show Tamagui theme/token values on hover 16 | 17 | - Graphical previews for color and theme tokens 18 | 19 | - Automatically reloads when your config is updated by the Tamagui compiler 20 | 21 | - Granular filtering options for autocomplete suggestions 22 | 23 | - Translations for shorthand properties (e.g. `p` -> `padding`) 24 | 25 | - Smart sorting of autocomplete suggestions: 26 | 27 | - Tokens with numbers get sorted in numeric order 28 | 29 | - Negative space tokens get sorted after postive ones 30 | 31 | - Theme tokens always come before any Color tokens 32 | 33 | - Unprefixed tokens sort before scale-specific tokens (`$scale.token`) 34 | 35 | ### Screenshots 36 | 37 | ##### Theme tokens 38 | 39 | ![Theme Token Screenshot](./docs/screenshot_autocomplete_theme.png) 40 | 41 | ![Translucent Color Screenshot](./docs/screenshot_autocomplete_translucent.png) 42 | 43 | ##### Quick info on hover 44 | 45 | ![Quick Info Color Hover Screenshot](./docs/screenshot_quickinfo_color.png) 46 | 47 | ![Quick Info Space Hover Screenshot](./docs/screenshot_quickinfo_space.png) 48 | 49 | ![Quick Info Shorthand Hover Screenshot](./docs/screenshot_quickinfo_shorthand.png) 50 | 51 | ##### Sorted space tokens 52 | 53 | ![Space Token Screenshot](./docs/screenshot_autocomplete_space.png) 54 | 55 | ### Changelog 56 | 57 | See the [changelog](./CHANGELOG.md) for the latest changes. 58 | 59 | ## Setup 60 | 61 | 1. Install `@nderscore/tamagui-typescript-plugin` package in your project: 62 | 63 | ```sh 64 | yarn add @nderscore/tamagui-typescript-plugin 65 | # or 66 | pnpm add @nderscore/tamagui-typescript-plugin 67 | # or 68 | npm add @nderscore/tamagui-typescript-plugin 69 | ``` 70 | 71 | 1. Add plugin to your `tsconfig.json` with settings: 72 | 73 | 74 | ```json5 75 | { 76 | "compilerOptions": { 77 | "plugins": [ 78 | { 79 | "name": "@nderscore/tamagui-typescript-plugin", 80 | // all settings are optional, the defaults are shown below as an example: 81 | // 82 | // relative or absolute path to a tamagui app (parent folder of .tamagui) 83 | "pathToApp": "apps/next", 84 | // 85 | // the default theme to show for theme tokens when inlined 86 | "defaultTheme": "light", 87 | // 88 | // the size (in pixels) of color tiles in previews 89 | "colorTileSize": 18, 90 | // 91 | // set false to hide translations for shorthand properties 92 | "showShorthandTranslations": true, 93 | // 94 | // options for filtering autocomplete suggestions 95 | "completionFilters": { 96 | // 97 | // set false to hide non-theme color tokens 98 | "showColorTokens": true, 99 | // 100 | // set false to hide $true and $-true tokens from all scales 101 | "showTrueTokens": true, 102 | // 103 | // per-scale lists of specific token names to exclude from autocomplete results 104 | "custom": { 105 | // "themeColor": [], 106 | // "color": [], 107 | // "size": [], 108 | // "space": [], 109 | // "radius": [], 110 | // "zIndex": [] 111 | } 112 | } 113 | } 114 | ] 115 | } 116 | } 117 | ``` 118 | 119 | **For monorepos:** 120 | 121 | In a monorepo, like the tamagui starter template, you may have multiple `tsconfig.json` for each of your workspace packages. If your packages' configurations extend from one or more base configurations, you should add the plugin to each one of those base configs and not to the individual packages. 122 | 123 | For the tamagui starter project, the base tsconfigs are: 124 | 125 | - `tsconfig.json` 126 | - `tsconfig.base.json` 127 | - `apps/next/tsconfig.json` 128 | 129 | 1. Make sure your VSCode is configured to use typescript from your workspace: 130 | 131 | - Open VSCode `Command Palette` -> `Select Typescript Version...` -> `Use Workspace Version` 132 | 133 | ### Usage in Expo-only (no-Next.js) Tamagui projects 134 | 135 | Currently, the `@tamagui/babel-plugin` may not generate a `.tamagui` directory with your configuration cached inside. 136 | 137 | If you encounter this issue, you can use a temporary workaround to generate it manually: 138 | 139 | #### Expo-only workaround 140 | 141 | 1. Add `@tamagui/static` to your project: 142 | 143 | ``` 144 | yarn add @tamagui/static 145 | ``` 146 | 147 | 1. Create a script `generate-tamagui-json.js` and fill in with your settings if needed: 148 | 149 | ```js 150 | // generate-tamagui-json.js 151 | const { loadTamagui } = require('@tamagui/static'); 152 | 153 | loadTamagui({ 154 | config: 'tamagui.config.ts', 155 | components: ['tamagui'], 156 | }); 157 | ``` 158 | 159 | 1. Execute `node generate-tamagui-json.js` to generate a `.tamagui` directory in your Expo project folder. 160 | 161 | This script will need to be ran manually after changing your theme/tokens. 162 | 163 | ## Contributing 164 | 165 | If you would like to contribute to this project, please see the [contributing guide](./CONTRIBUTING.md). 166 | -------------------------------------------------------------------------------- /docs/screenshot_autocomplete_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_autocomplete_space.png -------------------------------------------------------------------------------- /docs/screenshot_autocomplete_theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_autocomplete_theme.png -------------------------------------------------------------------------------- /docs/screenshot_autocomplete_translucent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_autocomplete_translucent.png -------------------------------------------------------------------------------- /docs/screenshot_quickinfo_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_quickinfo_color.png -------------------------------------------------------------------------------- /docs/screenshot_quickinfo_shorthand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_quickinfo_shorthand.png -------------------------------------------------------------------------------- /docs/screenshot_quickinfo_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nderscore/tamagui-typescript-plugin/eb4dbd4ea9a60cbfff2ffd9ae8992ec2e54c0b02/docs/screenshot_quickinfo_space.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nderscore/tamagui-typescript-plugin", 3 | "version": "0.6.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/nderscore/tamagui-typescript-plugin" 8 | }, 9 | "main": "./dist", 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "scripts": { 15 | "build": "tsc", 16 | "clean": "rm -rf ./dist", 17 | "cleanbuild": "pnpm run clean && pnpm run build", 18 | "changelog:major": "changelog -M --exclude 'chore,docs,ci'", 19 | "changelog:minor": "changelog -m --exclude 'chore,docs,ci'", 20 | "changelog:patch": "changelog -p --exclude 'chore,docs,ci'", 21 | "changelog:commit": "prettier -w CHANGELOG.md && git add CHANGELOG.md && git commit -m 'docs: Update CHANGELOG.md'", 22 | "release:major": "pnpm changelog:major && pnpm changelog:commit && pnpm version major", 23 | "release:minor": "pnpm changelog:minor && pnpm changelog:commit && pnpm version minor", 24 | "release:patch": "pnpm changelog:patch && pnpm changelog:commit && pnpm version patch" 25 | }, 26 | "dependencies": { 27 | "color": "4.2.3" 28 | }, 29 | "peerDependencies": { 30 | "@tamagui/core": "*", 31 | "@tamagui/helpers": "*", 32 | "typescript": ">=4.8" 33 | }, 34 | "devDependencies": { 35 | "@ianvs/prettier-plugin-sort-imports": "4.1.0", 36 | "@tamagui/core": "1.74.15", 37 | "@tamagui/helpers": "1.74.15", 38 | "@types/color": "3.0.4", 39 | "@types/node": "20.6.0", 40 | "generate-changelog": "1.8.0", 41 | "prettier": "3.0.3", 42 | "typescript": "5.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | color: 5 | specifier: 4.2.3 6 | version: 4.2.3 7 | 8 | devDependencies: 9 | '@ianvs/prettier-plugin-sort-imports': 10 | specifier: 4.1.0 11 | version: 4.1.0(prettier@3.0.3) 12 | '@tamagui/core': 13 | specifier: 1.74.15 14 | version: 1.74.15(react@18.2.0) 15 | '@tamagui/helpers': 16 | specifier: 1.74.15 17 | version: 1.74.15(react@18.2.0) 18 | '@types/color': 19 | specifier: 3.0.4 20 | version: 3.0.4 21 | '@types/node': 22 | specifier: 20.6.0 23 | version: 20.6.0 24 | generate-changelog: 25 | specifier: 1.8.0 26 | version: 1.8.0 27 | prettier: 28 | specifier: 3.0.3 29 | version: 3.0.3 30 | typescript: 31 | specifier: 5.2.2 32 | version: 5.2.2 33 | 34 | packages: 35 | 36 | /@ampproject/remapping@2.2.1: 37 | resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} 38 | engines: {node: '>=6.0.0'} 39 | dependencies: 40 | '@jridgewell/gen-mapping': 0.3.3 41 | '@jridgewell/trace-mapping': 0.3.19 42 | dev: true 43 | 44 | /@babel/code-frame@7.22.10: 45 | resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} 46 | engines: {node: '>=6.9.0'} 47 | dependencies: 48 | '@babel/highlight': 7.22.10 49 | chalk: 2.4.2 50 | dev: true 51 | 52 | /@babel/compat-data@7.22.9: 53 | resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} 54 | engines: {node: '>=6.9.0'} 55 | dev: true 56 | 57 | /@babel/core@7.22.10: 58 | resolution: {integrity: sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==} 59 | engines: {node: '>=6.9.0'} 60 | dependencies: 61 | '@ampproject/remapping': 2.2.1 62 | '@babel/code-frame': 7.22.10 63 | '@babel/generator': 7.22.10 64 | '@babel/helper-compilation-targets': 7.22.10 65 | '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.10) 66 | '@babel/helpers': 7.22.10 67 | '@babel/parser': 7.22.10 68 | '@babel/template': 7.22.5 69 | '@babel/traverse': 7.22.10 70 | '@babel/types': 7.22.10 71 | convert-source-map: 1.9.0 72 | debug: 4.3.4 73 | gensync: 1.0.0-beta.2 74 | json5: 2.2.3 75 | semver: 6.3.1 76 | transitivePeerDependencies: 77 | - supports-color 78 | dev: true 79 | 80 | /@babel/generator@7.22.10: 81 | resolution: {integrity: sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==} 82 | engines: {node: '>=6.9.0'} 83 | dependencies: 84 | '@babel/types': 7.22.10 85 | '@jridgewell/gen-mapping': 0.3.3 86 | '@jridgewell/trace-mapping': 0.3.19 87 | jsesc: 2.5.2 88 | dev: true 89 | 90 | /@babel/helper-compilation-targets@7.22.10: 91 | resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} 92 | engines: {node: '>=6.9.0'} 93 | dependencies: 94 | '@babel/compat-data': 7.22.9 95 | '@babel/helper-validator-option': 7.22.5 96 | browserslist: 4.21.10 97 | lru-cache: 5.1.1 98 | semver: 6.3.1 99 | dev: true 100 | 101 | /@babel/helper-environment-visitor@7.22.5: 102 | resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} 103 | engines: {node: '>=6.9.0'} 104 | dev: true 105 | 106 | /@babel/helper-function-name@7.22.5: 107 | resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} 108 | engines: {node: '>=6.9.0'} 109 | dependencies: 110 | '@babel/template': 7.22.5 111 | '@babel/types': 7.22.10 112 | dev: true 113 | 114 | /@babel/helper-hoist-variables@7.22.5: 115 | resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} 116 | engines: {node: '>=6.9.0'} 117 | dependencies: 118 | '@babel/types': 7.22.10 119 | dev: true 120 | 121 | /@babel/helper-module-imports@7.22.5: 122 | resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} 123 | engines: {node: '>=6.9.0'} 124 | dependencies: 125 | '@babel/types': 7.22.10 126 | dev: true 127 | 128 | /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.10): 129 | resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} 130 | engines: {node: '>=6.9.0'} 131 | peerDependencies: 132 | '@babel/core': ^7.0.0 133 | dependencies: 134 | '@babel/core': 7.22.10 135 | '@babel/helper-environment-visitor': 7.22.5 136 | '@babel/helper-module-imports': 7.22.5 137 | '@babel/helper-simple-access': 7.22.5 138 | '@babel/helper-split-export-declaration': 7.22.6 139 | '@babel/helper-validator-identifier': 7.22.5 140 | dev: true 141 | 142 | /@babel/helper-simple-access@7.22.5: 143 | resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} 144 | engines: {node: '>=6.9.0'} 145 | dependencies: 146 | '@babel/types': 7.22.10 147 | dev: true 148 | 149 | /@babel/helper-split-export-declaration@7.22.6: 150 | resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} 151 | engines: {node: '>=6.9.0'} 152 | dependencies: 153 | '@babel/types': 7.22.10 154 | dev: true 155 | 156 | /@babel/helper-string-parser@7.22.5: 157 | resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} 158 | engines: {node: '>=6.9.0'} 159 | dev: true 160 | 161 | /@babel/helper-validator-identifier@7.22.5: 162 | resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} 163 | engines: {node: '>=6.9.0'} 164 | dev: true 165 | 166 | /@babel/helper-validator-option@7.22.5: 167 | resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} 168 | engines: {node: '>=6.9.0'} 169 | dev: true 170 | 171 | /@babel/helpers@7.22.10: 172 | resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==} 173 | engines: {node: '>=6.9.0'} 174 | dependencies: 175 | '@babel/template': 7.22.5 176 | '@babel/traverse': 7.22.10 177 | '@babel/types': 7.22.10 178 | transitivePeerDependencies: 179 | - supports-color 180 | dev: true 181 | 182 | /@babel/highlight@7.22.10: 183 | resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} 184 | engines: {node: '>=6.9.0'} 185 | dependencies: 186 | '@babel/helper-validator-identifier': 7.22.5 187 | chalk: 2.4.2 188 | js-tokens: 4.0.0 189 | dev: true 190 | 191 | /@babel/parser@7.22.10: 192 | resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==} 193 | engines: {node: '>=6.0.0'} 194 | hasBin: true 195 | dependencies: 196 | '@babel/types': 7.22.10 197 | dev: true 198 | 199 | /@babel/template@7.22.5: 200 | resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} 201 | engines: {node: '>=6.9.0'} 202 | dependencies: 203 | '@babel/code-frame': 7.22.10 204 | '@babel/parser': 7.22.10 205 | '@babel/types': 7.22.10 206 | dev: true 207 | 208 | /@babel/traverse@7.22.10: 209 | resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==} 210 | engines: {node: '>=6.9.0'} 211 | dependencies: 212 | '@babel/code-frame': 7.22.10 213 | '@babel/generator': 7.22.10 214 | '@babel/helper-environment-visitor': 7.22.5 215 | '@babel/helper-function-name': 7.22.5 216 | '@babel/helper-hoist-variables': 7.22.5 217 | '@babel/helper-split-export-declaration': 7.22.6 218 | '@babel/parser': 7.22.10 219 | '@babel/types': 7.22.10 220 | debug: 4.3.4 221 | globals: 11.12.0 222 | transitivePeerDependencies: 223 | - supports-color 224 | dev: true 225 | 226 | /@babel/types@7.22.10: 227 | resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} 228 | engines: {node: '>=6.9.0'} 229 | dependencies: 230 | '@babel/helper-string-parser': 7.22.5 231 | '@babel/helper-validator-identifier': 7.22.5 232 | to-fast-properties: 2.0.0 233 | dev: true 234 | 235 | /@ianvs/prettier-plugin-sort-imports@4.1.0(prettier@3.0.3): 236 | resolution: {integrity: sha512-IAXeTLU24k6mRPa6mFbW1qZJ/j0m3OeH44wyijWyr+YqqdNtBnfHxAntOAATS9iDfrT01NesKGsdzqnXdDQa/A==} 237 | peerDependencies: 238 | '@vue/compiler-sfc': '>=3.0.0' 239 | prettier: 2 || 3 240 | peerDependenciesMeta: 241 | '@vue/compiler-sfc': 242 | optional: true 243 | dependencies: 244 | '@babel/core': 7.22.10 245 | '@babel/generator': 7.22.10 246 | '@babel/parser': 7.22.10 247 | '@babel/traverse': 7.22.10 248 | '@babel/types': 7.22.10 249 | prettier: 3.0.3 250 | semver: 7.5.4 251 | transitivePeerDependencies: 252 | - supports-color 253 | dev: true 254 | 255 | /@jridgewell/gen-mapping@0.3.3: 256 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 257 | engines: {node: '>=6.0.0'} 258 | dependencies: 259 | '@jridgewell/set-array': 1.1.2 260 | '@jridgewell/sourcemap-codec': 1.4.15 261 | '@jridgewell/trace-mapping': 0.3.19 262 | dev: true 263 | 264 | /@jridgewell/resolve-uri@3.1.1: 265 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 266 | engines: {node: '>=6.0.0'} 267 | dev: true 268 | 269 | /@jridgewell/set-array@1.1.2: 270 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 271 | engines: {node: '>=6.0.0'} 272 | dev: true 273 | 274 | /@jridgewell/sourcemap-codec@1.4.15: 275 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 276 | dev: true 277 | 278 | /@jridgewell/trace-mapping@0.3.19: 279 | resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} 280 | dependencies: 281 | '@jridgewell/resolve-uri': 3.1.1 282 | '@jridgewell/sourcemap-codec': 1.4.15 283 | dev: true 284 | 285 | /@react-native/normalize-color@2.1.0: 286 | resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==} 287 | dev: true 288 | 289 | /@tamagui/compose-refs@1.74.15(react@18.2.0): 290 | resolution: {integrity: sha512-AKT8HS365C7QN5MBUW0crjviJArA5xD4cupAB0J19UPDQyFnaWXgJBfGqqtp3FxXapCacex2LwEPNs2pn/JbcA==} 291 | peerDependencies: 292 | react: '*' 293 | dependencies: 294 | react: 18.2.0 295 | dev: true 296 | 297 | /@tamagui/constants@1.74.15(react@18.2.0): 298 | resolution: {integrity: sha512-9UEiu8sUWissTYaTaeJomrrS/q4g1JfJQXFHJ8e8cXJfWi7Yp4dGO63iaC78t7+T/Li+lrretmeIc8kVGqQyvg==} 299 | peerDependencies: 300 | react: '*' 301 | dependencies: 302 | react: 18.2.0 303 | dev: true 304 | 305 | /@tamagui/core@1.74.15(react@18.2.0): 306 | resolution: {integrity: sha512-ZIREr9B6EFc8kwiTkwMjdh8NBGeo0pWs6KGiDGnXjqgQ68v82yRPLxG4Y3e1KtIlPP7dhv8MePk+++xJrgDjLw==} 307 | peerDependencies: 308 | react: '*' 309 | dependencies: 310 | '@tamagui/react-native-use-pressable': 1.74.15(react@18.2.0) 311 | '@tamagui/react-native-use-responder-events': 1.74.15(react@18.2.0) 312 | '@tamagui/use-event': 1.74.15(react@18.2.0) 313 | '@tamagui/web': 1.74.15(react@18.2.0) 314 | react: 18.2.0 315 | dev: true 316 | 317 | /@tamagui/helpers@1.74.15(react@18.2.0): 318 | resolution: {integrity: sha512-xTGhaXThwlW90rDGOgJF1NTnhBqg26CZi+uvIpR+1/+q3kqFjknk4aV9zQlFssh9odvkhtIOGh9T8oRfrbiIhg==} 319 | dependencies: 320 | '@tamagui/constants': 1.74.15(react@18.2.0) 321 | '@tamagui/simple-hash': 1.74.15 322 | transitivePeerDependencies: 323 | - react 324 | dev: true 325 | 326 | /@tamagui/normalize-css-color@1.74.15: 327 | resolution: {integrity: sha512-g5yHeKODRJ7jOem3YYUrSpKqw07+/Qno49xMxWyGKhFc75py6dzexJjEgBQA2WikWJwEjdvej/TukApzZv4zvA==} 328 | dependencies: 329 | '@react-native/normalize-color': 2.1.0 330 | dev: true 331 | 332 | /@tamagui/react-native-use-pressable@1.74.15(react@18.2.0): 333 | resolution: {integrity: sha512-do+Em7BID2oMQQaXj6GM9B9iHDHeajnv2LOHfzOfla0eE9WUrV4HcAXWXW9FHoRLwOEg/Gx4yw3a6hMM6lNRyQ==} 334 | peerDependencies: 335 | react: '*' 336 | dependencies: 337 | react: 18.2.0 338 | dev: true 339 | 340 | /@tamagui/react-native-use-responder-events@1.74.15(react@18.2.0): 341 | resolution: {integrity: sha512-MLvD8Ot13pPf4HcqoPFWIbXK2t4JtNIRzukS9DceFETpSZ4SlgA0yBaEzq3W6434zMfIW8n3PGY0FEiBhCWo5w==} 342 | peerDependencies: 343 | react: '*' 344 | dependencies: 345 | react: 18.2.0 346 | dev: true 347 | 348 | /@tamagui/simple-hash@1.74.15: 349 | resolution: {integrity: sha512-K7KDGDCIqXq++IA9I1PhMR/wuXkGoNuFZUDjCnC316450zwB5DbSAt7kLfBxcnnpgvYDoGu7t0S/ooBKYzQsYA==} 350 | dev: true 351 | 352 | /@tamagui/timer@1.74.15: 353 | resolution: {integrity: sha512-Wpv9eQ+WhfBxf/1Cc82+pIhbazRRQGZ+SRLv1HVvRBIU8xApuOApNSn3QxSy972nmzlHU6a5hBrjJBS7gT0I+Q==} 354 | dev: true 355 | 356 | /@tamagui/use-did-finish-ssr@1.74.15(react@18.2.0): 357 | resolution: {integrity: sha512-4nwjagXyyNixgColu+IGnkSVEqVx9OsQWdBTriqpPe9yHM4VuMmPZDa0YZ84gtmjCJgDWWH7u+HmelmEjLjCTg==} 358 | peerDependencies: 359 | react: '*' 360 | dependencies: 361 | '@tamagui/constants': 1.74.15(react@18.2.0) 362 | react: 18.2.0 363 | dev: true 364 | 365 | /@tamagui/use-event@1.74.15(react@18.2.0): 366 | resolution: {integrity: sha512-/S09ooCHiemd1MkXTsHnbaRyrKsvysGPMkPe4ED/3LlCV6FOyX24pJl2YJ5buNWYmnwYqubYDtwxFHlOnYgSgg==} 367 | peerDependencies: 368 | react: '*' 369 | dependencies: 370 | react: 18.2.0 371 | dev: true 372 | 373 | /@tamagui/use-force-update@1.74.15(react@18.2.0): 374 | resolution: {integrity: sha512-Rxa18Lcz9q7j32/2v/tMTX4aZSFjShw4/qFul8wGMgD1r9dq5tYy0d1cV/2rbMkJ2G6PySqmnPpuiaQRF5dmXg==} 375 | peerDependencies: 376 | react: '*' 377 | dependencies: 378 | react: 18.2.0 379 | dev: true 380 | 381 | /@tamagui/web@1.74.15(react@18.2.0): 382 | resolution: {integrity: sha512-xbcmS4+BjhD4dWBkIAs70hUeyi3aI6XZCOXJSmH2WjsVhZ7sw4GKDK/+sS5SUNrOTHJxBFB6j0RaK88tleyn3g==} 383 | peerDependencies: 384 | react: '*' 385 | dependencies: 386 | '@tamagui/compose-refs': 1.74.15(react@18.2.0) 387 | '@tamagui/constants': 1.74.15(react@18.2.0) 388 | '@tamagui/helpers': 1.74.15(react@18.2.0) 389 | '@tamagui/normalize-css-color': 1.74.15 390 | '@tamagui/timer': 1.74.15 391 | '@tamagui/use-did-finish-ssr': 1.74.15(react@18.2.0) 392 | '@tamagui/use-force-update': 1.74.15(react@18.2.0) 393 | react: 18.2.0 394 | dev: true 395 | 396 | /@types/color-convert@2.0.1: 397 | resolution: {integrity: sha512-GwXanrvq/tBHJtudbl1lSy9Ybt7KS9+rA+YY3bcuIIM+d6jSHUr+5yjO83gtiRpuaPiBccwFjSnAK2qSrIPA7w==} 398 | dependencies: 399 | '@types/color-name': 1.1.1 400 | dev: true 401 | 402 | /@types/color-name@1.1.1: 403 | resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==} 404 | dev: true 405 | 406 | /@types/color@3.0.4: 407 | resolution: {integrity: sha512-OpisS4bqJJwbkkQRrMvURf3DOxBoAg9mysHYI7WgrWpSYHqHGKYBULHdz4ih77SILcLDo/zyHGFyfIl9yb8NZQ==} 408 | dependencies: 409 | '@types/color-convert': 2.0.1 410 | dev: true 411 | 412 | /@types/node@20.6.0: 413 | resolution: {integrity: sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==} 414 | dev: true 415 | 416 | /ansi-styles@3.2.1: 417 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 418 | engines: {node: '>=4'} 419 | dependencies: 420 | color-convert: 1.9.3 421 | dev: true 422 | 423 | /bluebird@3.7.2: 424 | resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} 425 | dev: true 426 | 427 | /browserslist@4.21.10: 428 | resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} 429 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 430 | hasBin: true 431 | dependencies: 432 | caniuse-lite: 1.0.30001519 433 | electron-to-chromium: 1.4.490 434 | node-releases: 2.0.13 435 | update-browserslist-db: 1.0.11(browserslist@4.21.10) 436 | dev: true 437 | 438 | /caniuse-lite@1.0.30001519: 439 | resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==} 440 | dev: true 441 | 442 | /chalk@2.4.2: 443 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 444 | engines: {node: '>=4'} 445 | dependencies: 446 | ansi-styles: 3.2.1 447 | escape-string-regexp: 1.0.5 448 | supports-color: 5.5.0 449 | dev: true 450 | 451 | /color-convert@1.9.3: 452 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 453 | dependencies: 454 | color-name: 1.1.3 455 | dev: true 456 | 457 | /color-convert@2.0.1: 458 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 459 | engines: {node: '>=7.0.0'} 460 | dependencies: 461 | color-name: 1.1.4 462 | dev: false 463 | 464 | /color-name@1.1.3: 465 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 466 | 467 | /color-name@1.1.4: 468 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 469 | dev: false 470 | 471 | /color-string@1.9.1: 472 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 473 | dependencies: 474 | color-name: 1.1.3 475 | simple-swizzle: 0.2.2 476 | dev: false 477 | 478 | /color@4.2.3: 479 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 480 | engines: {node: '>=12.5.0'} 481 | dependencies: 482 | color-convert: 2.0.1 483 | color-string: 1.9.1 484 | dev: false 485 | 486 | /commander@2.20.3: 487 | resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} 488 | dev: true 489 | 490 | /convert-source-map@1.9.0: 491 | resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} 492 | dev: true 493 | 494 | /debug@4.3.4: 495 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 496 | engines: {node: '>=6.0'} 497 | peerDependencies: 498 | supports-color: '*' 499 | peerDependenciesMeta: 500 | supports-color: 501 | optional: true 502 | dependencies: 503 | ms: 2.1.2 504 | dev: true 505 | 506 | /electron-to-chromium@1.4.490: 507 | resolution: {integrity: sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==} 508 | dev: true 509 | 510 | /escalade@3.1.1: 511 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 512 | engines: {node: '>=6'} 513 | dev: true 514 | 515 | /escape-string-regexp@1.0.5: 516 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 517 | engines: {node: '>=0.8.0'} 518 | dev: true 519 | 520 | /generate-changelog@1.8.0: 521 | resolution: {integrity: sha512-msgpxeB75Ziyg3wGsZuPNl7c5RxChMKmYcAX5obnhUow90dBZW3nLic6nxGtst7Bpx453oS6zAIHcX7F3QVasw==} 522 | hasBin: true 523 | dependencies: 524 | bluebird: 3.7.2 525 | commander: 2.20.3 526 | github-url-from-git: 1.5.0 527 | dev: true 528 | 529 | /gensync@1.0.0-beta.2: 530 | resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 531 | engines: {node: '>=6.9.0'} 532 | dev: true 533 | 534 | /github-url-from-git@1.5.0: 535 | resolution: {integrity: sha512-WWOec4aRI7YAykQ9+BHmzjyNlkfJFG8QLXnDTsLz/kZefq7qkzdfo4p6fkYYMIq1aj+gZcQs/1HQhQh3DPPxlQ==} 536 | dev: true 537 | 538 | /globals@11.12.0: 539 | resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} 540 | engines: {node: '>=4'} 541 | dev: true 542 | 543 | /has-flag@3.0.0: 544 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 545 | engines: {node: '>=4'} 546 | dev: true 547 | 548 | /is-arrayish@0.3.2: 549 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 550 | dev: false 551 | 552 | /js-tokens@4.0.0: 553 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 554 | dev: true 555 | 556 | /jsesc@2.5.2: 557 | resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} 558 | engines: {node: '>=4'} 559 | hasBin: true 560 | dev: true 561 | 562 | /json5@2.2.3: 563 | resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 564 | engines: {node: '>=6'} 565 | hasBin: true 566 | dev: true 567 | 568 | /loose-envify@1.4.0: 569 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 570 | hasBin: true 571 | dependencies: 572 | js-tokens: 4.0.0 573 | dev: true 574 | 575 | /lru-cache@5.1.1: 576 | resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 577 | dependencies: 578 | yallist: 3.1.1 579 | dev: true 580 | 581 | /lru-cache@6.0.0: 582 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 583 | engines: {node: '>=10'} 584 | dependencies: 585 | yallist: 4.0.0 586 | dev: true 587 | 588 | /ms@2.1.2: 589 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 590 | dev: true 591 | 592 | /node-releases@2.0.13: 593 | resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} 594 | dev: true 595 | 596 | /picocolors@1.0.0: 597 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 598 | dev: true 599 | 600 | /prettier@3.0.3: 601 | resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} 602 | engines: {node: '>=14'} 603 | hasBin: true 604 | dev: true 605 | 606 | /react@18.2.0: 607 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 608 | engines: {node: '>=0.10.0'} 609 | dependencies: 610 | loose-envify: 1.4.0 611 | dev: true 612 | 613 | /semver@6.3.1: 614 | resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 615 | hasBin: true 616 | dev: true 617 | 618 | /semver@7.5.4: 619 | resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} 620 | engines: {node: '>=10'} 621 | hasBin: true 622 | dependencies: 623 | lru-cache: 6.0.0 624 | dev: true 625 | 626 | /simple-swizzle@0.2.2: 627 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 628 | dependencies: 629 | is-arrayish: 0.3.2 630 | dev: false 631 | 632 | /supports-color@5.5.0: 633 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 634 | engines: {node: '>=4'} 635 | dependencies: 636 | has-flag: 3.0.0 637 | dev: true 638 | 639 | /to-fast-properties@2.0.0: 640 | resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} 641 | engines: {node: '>=4'} 642 | dev: true 643 | 644 | /typescript@5.2.2: 645 | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 646 | engines: {node: '>=14.17'} 647 | hasBin: true 648 | dev: true 649 | 650 | /update-browserslist-db@1.0.11(browserslist@4.21.10): 651 | resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} 652 | hasBin: true 653 | peerDependencies: 654 | browserslist: '>= 4.21.0' 655 | dependencies: 656 | browserslist: 4.21.10 657 | escalade: 3.1.1 658 | picocolors: 1.0.0 659 | dev: true 660 | 661 | /yallist@3.1.1: 662 | resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 663 | dev: true 664 | 665 | /yallist@4.0.0: 666 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 667 | dev: true 668 | -------------------------------------------------------------------------------- /src/getCompletionDetails.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { getTokenType } from './getTokens'; 4 | import { 5 | makeColorTokenDescription, 6 | makeShorthandDescription, 7 | makeThemeTokenDescription, 8 | makeTokenDescription, 9 | } from './metadata'; 10 | import { ParsedConfig } from './readConfig'; 11 | import { PluginOptions } from './readOptions'; 12 | import { safeInsertDocs } from './safeInsertDocs'; 13 | import { TSContext } from './types'; 14 | import { 15 | getMaybeSpecificToken, 16 | sanitizeMaybeQuotedString, 17 | toPascal, 18 | } from './utils'; 19 | 20 | /** 21 | * Hook to augment completion details with markdown documentation 22 | */ 23 | export const getCompletionDetails = ( 24 | original: ts.CompletionEntryDetails, 25 | { 26 | fileName, 27 | position, 28 | entryName, 29 | // formatOptions, 30 | // source, 31 | // preferences, 32 | // data, 33 | ctx, 34 | config, 35 | options, 36 | }: { 37 | entryName: string; 38 | fileName: string; 39 | position: number; 40 | formatOptions?: ts.FormatCodeOptions | ts.FormatCodeSettings; 41 | source?: string; 42 | preferences?: ts.UserPreferences; 43 | data?: ts.CompletionEntryData; 44 | ctx: TSContext; 45 | config: ParsedConfig; 46 | options: PluginOptions; 47 | } 48 | ): ts.CompletionEntryDetails => { 49 | const { logger } = ctx; 50 | 51 | logger(`calculating tamagui completion details <$${position}@${fileName}>`); 52 | 53 | const tokenType = getTokenType(fileName, position, config, ctx); 54 | 55 | if (!tokenType) return original; 56 | const { type, shorthand, prop, isShorthand } = tokenType; 57 | 58 | logger(`token type <${type}>`); 59 | 60 | const sanitizedEntryName = sanitizeMaybeQuotedString(entryName); 61 | 62 | if (isShorthand && shorthand && options.showShorthandTranslations) { 63 | safeInsertDocs( 64 | original, 65 | 'shorthand', 66 | makeShorthandDescription(shorthand, prop) 67 | ); 68 | } 69 | 70 | let foundThemeColor = false; 71 | if (type === 'color') { 72 | const themeValue = config.themeColors[sanitizedEntryName]; 73 | if (themeValue) { 74 | safeInsertDocs( 75 | original, 76 | 'token', 77 | makeThemeTokenDescription(themeValue, options) 78 | ); 79 | foundThemeColor = true; 80 | } 81 | } 82 | 83 | const [scale, value] = !foundThemeColor 84 | ? getMaybeSpecificToken(entryName, type, config) 85 | : []; 86 | if (scale && value) { 87 | const isColor = scale === 'color'; 88 | safeInsertDocs( 89 | original, 90 | 'token', 91 | isColor 92 | ? makeColorTokenDescription(value, options) 93 | : makeTokenDescription(toPascal(scale), value) 94 | ); 95 | } 96 | 97 | return original; 98 | }; 99 | -------------------------------------------------------------------------------- /src/getCompletions.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { getTokenType } from './getTokens'; 4 | import { ParsedConfig } from './readConfig'; 5 | import { PluginOptions } from './readOptions'; 6 | import { TSContext } from './types'; 7 | import { 8 | getMaybeSpecificToken, 9 | getSortText, 10 | sanitizeMaybeQuotedString, 11 | toPascal, 12 | } from './utils'; 13 | 14 | /** 15 | * Hook to augment completion results with tamagui tokens 16 | */ 17 | export const getCompletions = ( 18 | original: ts.WithMetadata, 19 | { 20 | fileName, 21 | position, 22 | // opts, 23 | ctx, 24 | config, 25 | options, 26 | }: { 27 | fileName: string; 28 | position: number; 29 | opts?: ts.GetCompletionsAtPositionOptions; 30 | ctx: TSContext; 31 | config: ParsedConfig; 32 | options: PluginOptions; 33 | } 34 | ): ts.WithMetadata => { 35 | const { logger } = ctx; 36 | const { defaultTheme } = options; 37 | 38 | logger(`calculating tamagui completions <$${position}@${fileName}>`); 39 | 40 | const tokenType = getTokenType(fileName, position, config, ctx); 41 | 42 | if (!tokenType) return original; 43 | const { type } = tokenType; 44 | 45 | logger(`token type <${type}>`); 46 | 47 | const entries: ts.CompletionEntry[] = []; 48 | for (const entry of original.entries) { 49 | if (type === 'color') { 50 | const sanitizedEntryName = sanitizeMaybeQuotedString(entry.name); 51 | const themeValue = config.themeColors[sanitizedEntryName]; 52 | if (themeValue) { 53 | if ( 54 | options.completionFilters.custom?.('themeColor', sanitizedEntryName) 55 | ) { 56 | // custom filter 57 | continue; 58 | } 59 | const defaultValue = 60 | themeValue[defaultTheme] ?? Object.values(themeValue)[0]; 61 | entry.kindModifiers = 'color'; 62 | // add an extra '$' to prioritize theme tokens over color tokens 63 | entry.sortText = '$' + getSortText(entry.name); 64 | entry.labelDetails = { 65 | detail: ' ' + defaultValue, 66 | description: 'ThemeToken', 67 | }; 68 | entries.push(entry); 69 | continue; 70 | } 71 | } 72 | 73 | const [scale, value, token] = getMaybeSpecificToken( 74 | entry.name, 75 | type, 76 | config 77 | ); 78 | if (scale && value) { 79 | if (options.completionFilters.custom?.(scale, token)) { 80 | // custom filter 81 | continue; 82 | } 83 | if ( 84 | !options.completionFilters.showTrueTokens && 85 | (token === '$true' || token === '$-true') 86 | ) { 87 | // filter out true and -true tokens 88 | continue; 89 | } 90 | const isColor = scale === 'color'; 91 | if (isColor && !options.completionFilters.showColorTokens) { 92 | // filter out color tokens if the option is disabled 93 | continue; 94 | } 95 | if (isColor) { 96 | entry.kindModifiers = 'color'; 97 | } 98 | entry.sortText = getSortText(entry.name); 99 | entry.labelDetails = { 100 | detail: ' ' + value, 101 | description: `${toPascal(scale)}Token`, 102 | }; 103 | } 104 | 105 | entries.push(entry); 106 | } 107 | 108 | original.entries = entries; 109 | 110 | return original; 111 | }; 112 | -------------------------------------------------------------------------------- /src/getQuickInfo.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { getTokenWithValue } from './getTokens'; 4 | import { 5 | makeColorTokenDescription, 6 | makeShorthandDescription, 7 | makeThemeTokenDescription, 8 | makeTokenDescription, 9 | } from './metadata'; 10 | import { ParsedConfig } from './readConfig'; 11 | import { PluginOptions } from './readOptions'; 12 | import { safeInsertDocs } from './safeInsertDocs'; 13 | import { TSContext } from './types'; 14 | import { 15 | getMaybeSpecificToken, 16 | sanitizeMaybeQuotedString, 17 | toPascal, 18 | } from './utils'; 19 | 20 | export const getQuickInfo = ( 21 | original: ts.QuickInfo | undefined, 22 | { 23 | fileName, 24 | position, 25 | ctx, 26 | config, 27 | options, 28 | }: { 29 | fileName: string; 30 | position: number; 31 | ctx: TSContext; 32 | config: ParsedConfig; 33 | options: PluginOptions; 34 | } 35 | ): ts.QuickInfo | undefined => { 36 | const { logger } = ctx; 37 | 38 | logger(`calculating tamagui quick info <$${position}@${fileName}>`); 39 | 40 | const tokenResult = getTokenWithValue(fileName, position, config, ctx); 41 | 42 | if (!tokenResult) return original; 43 | 44 | const [{ type, isShorthand, shorthand, prop }, entryName, textSpan] = 45 | tokenResult; 46 | 47 | logger(`Logging shorthand and prop ${shorthand} ${prop}`); 48 | 49 | logger(`found token <${entryName}> of type <${type}>`); 50 | 51 | let touched = false; 52 | const result: ts.QuickInfo = original ?? { 53 | kind: ts.ScriptElementKind.string, 54 | kindModifiers: '', 55 | textSpan, 56 | }; 57 | 58 | const sanitizedEntryName = sanitizeMaybeQuotedString(entryName); 59 | 60 | if (isShorthand && shorthand && options.showShorthandTranslations) { 61 | safeInsertDocs( 62 | result, 63 | 'shorthand', 64 | makeShorthandDescription(shorthand, prop) 65 | ); 66 | touched = true; 67 | } 68 | 69 | let foundThemeColor = false; 70 | if (type === 'color') { 71 | const themeValue = config.themeColors[sanitizedEntryName]; 72 | if (themeValue) { 73 | result.kindModifiers = 'color'; 74 | safeInsertDocs( 75 | result, 76 | 'token', 77 | makeThemeTokenDescription(themeValue, options) 78 | ); 79 | foundThemeColor = true; 80 | touched = true; 81 | } 82 | } 83 | 84 | const [scale, value] = !foundThemeColor 85 | ? getMaybeSpecificToken(entryName, type, config) 86 | : []; 87 | if (scale && value) { 88 | const isColor = scale === 'color'; 89 | if (isColor) { 90 | result.kindModifiers = 'color'; 91 | } 92 | safeInsertDocs( 93 | result, 94 | 'token', 95 | isColor 96 | ? makeColorTokenDescription(value, options) 97 | : makeTokenDescription(toPascal(scale), value) 98 | ); 99 | touched = true; 100 | } 101 | 102 | // avoid returning our custom result if we didn't touch it 103 | return touched ? result : original; 104 | }; 105 | -------------------------------------------------------------------------------- /src/getTokens.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { mapPropToToken } from './mapPropToToken'; 4 | import { ParsedConfig } from './readConfig'; 5 | import { TSContext } from './types'; 6 | 7 | /** 8 | * Create a stack of all the nodes at a given position 9 | */ 10 | const getNodeTreeAtPosition = ( 11 | node: ts.Node, 12 | pos: number, 13 | tree: ts.Node[] = [] 14 | ): ts.Node[] => { 15 | const children = node.getChildren(); 16 | for (const child of children) { 17 | const start = child.getStart(); 18 | const end = child.getEnd(); 19 | if (start <= pos && pos <= end) { 20 | tree.push(child); 21 | return getNodeTreeAtPosition(child, pos, tree); 22 | } 23 | } 24 | return tree; 25 | }; 26 | 27 | /** 28 | * Determines whether a given type is a TamaguiComponent or similar type 29 | */ 30 | const isTamaguiComponentType = (nodeType: ts.Type) => { 31 | if (nodeType.aliasSymbol?.escapedName === 'TamaguiComponent') { 32 | // styled() component 33 | return true; 34 | } 35 | if (nodeType.getProperty('staticConfig')) { 36 | // styleable() component 37 | return true; 38 | } 39 | return false; 40 | }; 41 | 42 | /** 43 | * Determines whether to show tamagui completions at a given position and if 44 | * so, which type of tokens to show. 45 | * 46 | * Walks up the AST to determine whether the cursor position either is or is 47 | * a descendant of a node that is one of the following: 48 | * 49 | * - A property assignment inside a Tamagui styled() call 50 | * - A property assignment inside a Tamagui JSX element 51 | * - Or a JSX attribute inside a Tamagui JSX element 52 | * 53 | * If a valid property assignment or JSX attribute is found, it's name 54 | * determines the token type based on the property name. 55 | */ 56 | const getTokenAtPosition = ( 57 | fileName: string, 58 | position: number, 59 | ctx: TSContext 60 | ) => { 61 | const { program, typeChecker, logger } = ctx; 62 | const sourceFile = program.getSourceFile(fileName); 63 | if (!sourceFile) return undefined; 64 | logger(`Found source file: ${sourceFile.fileName}`); 65 | 66 | const nodeTree = getNodeTreeAtPosition(sourceFile, position); 67 | 68 | if (nodeTree.length < 2) return undefined; 69 | 70 | let i = nodeTree.length - 1; 71 | const originNode = nodeTree[i]; 72 | let valueNode: ts.Node | undefined; 73 | 74 | let isInTamaguiScope = false; 75 | let prop = ''; 76 | let value = ''; 77 | 78 | let node: ts.Node; 79 | let done = false; 80 | while (!done && i >= 0) { 81 | node = nodeTree[i--]!; 82 | const nodeType = ctx.typeChecker.getTypeAtLocation(node); 83 | const isStringLiteral = nodeType.isStringLiteral(); 84 | 85 | const isJsxAttribute = node.kind === ts.SyntaxKind.JsxAttribute; 86 | const isPropertyAssignment = node.kind === ts.SyntaxKind.PropertyAssignment; 87 | const isJsxOpeningElement = node.kind === ts.SyntaxKind.JsxOpeningElement; 88 | const isJsxSelfClosingElement = 89 | node.kind === ts.SyntaxKind.JsxSelfClosingElement; 90 | const isCallExpression = node.kind === ts.SyntaxKind.CallExpression; 91 | const isStyledCall = 92 | isCallExpression && node.getChildAt(0).getText() === 'styled'; 93 | 94 | // logger(` 95 | // node <${node.getText()}> 96 | // nodeKind <${node.kind}> 97 | // nodeType <${nodeType.aliasSymbol?.escapedName}> 98 | // nodeTypeFlags <${nodeType.flags.toString()}> 99 | // isJsxAttribute <${isJsxAttribute}> 100 | // isPropertyAssignment <${isPropertyAssignment}> 101 | // isJsxOpeningElement <${isJsxOpeningElement}> 102 | // isJsxSelfClosingElement <${isJsxSelfClosingElement}> 103 | // isCallExpression <${isCallExpression}> 104 | // isStyledCall <${isStyledCall}> 105 | // `); 106 | 107 | if (!prop && !valueNode && isStringLiteral) { 108 | valueNode = node; 109 | if (isJsxAttribute) { 110 | value = node.getChildAt(2).getText(); 111 | } else { 112 | value = node.getText(); 113 | } 114 | } 115 | if (!prop && isPropertyAssignment) { 116 | prop = node.getChildAt(0).getText(); 117 | } 118 | if (!prop && isJsxAttribute) { 119 | prop = node.getChildAt(0).getText(); 120 | } 121 | if (isJsxOpeningElement || isJsxSelfClosingElement) { 122 | done = true; 123 | const tagName = node.getChildAt(1); 124 | const tagType = typeChecker.getTypeAtLocation(tagName); 125 | const isTamaguiComponentTag = isTamaguiComponentType(tagType); 126 | if (isTamaguiComponentTag) { 127 | isInTamaguiScope = true; 128 | } else { 129 | logger(`Non tamagui component type <${node.getText()}>`); 130 | } 131 | } 132 | if (isStyledCall) { 133 | done = true; 134 | isInTamaguiScope = true; 135 | } 136 | } 137 | 138 | logger(`Origin node <${originNode?.getText()}>`); 139 | logger(`Value node <${valueNode?.getText()}>`); 140 | logger(`Prop <${prop}>`); 141 | logger(`Value <${value}>`); 142 | logger(`Detected tamagui scope <${isInTamaguiScope}>`); 143 | 144 | if (!isInTamaguiScope || !prop) return undefined; 145 | 146 | return [prop, value, valueNode] as const; 147 | }; 148 | 149 | export const getTokenType = ( 150 | fileName: string, 151 | position: number, 152 | config: ParsedConfig, 153 | ctx: TSContext 154 | ) => { 155 | const tokenMatch = getTokenAtPosition(fileName, position, ctx); 156 | if (!tokenMatch) return undefined; 157 | const [prop] = tokenMatch; 158 | return mapPropToToken(prop, config); 159 | }; 160 | 161 | export const getTokenWithValue = ( 162 | fileName: string, 163 | position: number, 164 | config: ParsedConfig, 165 | ctx: TSContext 166 | ) => { 167 | const tokenMatch = getTokenAtPosition(fileName, position, ctx); 168 | if (!tokenMatch) return undefined; 169 | const [prop, value, node] = tokenMatch; 170 | if (!value || !node) return undefined; 171 | return [ 172 | mapPropToToken(prop, config), 173 | value, 174 | { 175 | start: node.end - value.length, 176 | length: value.length, 177 | }, 178 | ] as const; 179 | }; 180 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { getCompletionDetails } from './getCompletionDetails'; 4 | import { getCompletions } from './getCompletions'; 5 | import { getQuickInfo } from './getQuickInfo'; 6 | import { ParsedConfig } from './readConfig'; 7 | import { PluginOptions } from './readOptions'; 8 | import { TSContext, TSContextBase } from './types'; 9 | 10 | /** 11 | * Binding code for hooks into the language server 12 | */ 13 | export const getLanguageServerHooks = ({ 14 | config, 15 | options, 16 | getContext, 17 | }: { 18 | config: ParsedConfig; 19 | options: PluginOptions; 20 | getContext: () => TSContext | TSContextBase; 21 | }) => { 22 | const languageServerHooks: Partial = { 23 | // 24 | getCompletionEntryDetails( 25 | fileName, 26 | position, 27 | entryName, 28 | formatOptions, 29 | source, 30 | preferences, 31 | data 32 | ) { 33 | const ctx = getContext(); 34 | const { info, logger } = ctx; 35 | const original = info.languageService.getCompletionEntryDetails( 36 | fileName, 37 | position, 38 | entryName, 39 | formatOptions, 40 | source, 41 | preferences, 42 | data 43 | ); 44 | 45 | if (!('program' in ctx)) { 46 | logger.error(`Completions details: No program found in context`); 47 | return original; 48 | } 49 | 50 | if (!original) return undefined; 51 | 52 | return getCompletionDetails(original, { 53 | fileName, 54 | position, 55 | entryName, 56 | formatOptions, 57 | source, 58 | preferences, 59 | data, 60 | ctx, 61 | config, 62 | options, 63 | }); 64 | }, 65 | // 66 | getCompletionsAtPosition(fileName, position, opts) { 67 | const ctx = getContext(); 68 | const { info, logger } = ctx; 69 | const original = info.languageService.getCompletionsAtPosition( 70 | fileName, 71 | position, 72 | opts 73 | ); 74 | 75 | if (!('program' in ctx)) { 76 | logger.error(`Completions: No program found in context`); 77 | return original; 78 | } 79 | 80 | if (!original) return undefined; 81 | 82 | return getCompletions(original, { 83 | fileName, 84 | position, 85 | opts, 86 | ctx, 87 | config, 88 | options, 89 | }); 90 | }, 91 | // 92 | getQuickInfoAtPosition(fileName, position) { 93 | const ctx = getContext(); 94 | const { info, logger } = ctx; 95 | const original = info.languageService.getQuickInfoAtPosition( 96 | fileName, 97 | position 98 | ); 99 | 100 | if (!('program' in ctx)) { 101 | logger.error(`No program found in context`); 102 | return original; 103 | } 104 | 105 | return getQuickInfo(original, { 106 | fileName, 107 | position, 108 | ctx, 109 | config, 110 | options, 111 | }); 112 | }, 113 | // 114 | }; 115 | 116 | return languageServerHooks; 117 | }; 118 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { getLanguageServerHooks } from './hooks'; 4 | import { readConfig } from './readConfig'; 5 | import { readOptions } from './readOptions'; 6 | import { TSContext, TSContextBase, tss } from './types'; 7 | 8 | const init = (modules: { typescript: tss }) => { 9 | const plugin: ts.server.PluginModule = { 10 | create(info: ts.server.PluginCreateInfo): ts.LanguageService { 11 | const logger = (msg: string | {}, type = 'info' as 'info' | 'error') => { 12 | const payload = 13 | typeof msg === 'string' 14 | ? msg 15 | : `(json)\n${JSON.stringify( 16 | msg, 17 | (_key, val) => 18 | typeof val === 'function' ? val.toString() : val, 19 | 2 20 | )}`; 21 | info.project.projectService.logger[type === 'info' ? 'info' : 'msg']( 22 | `TSTamagui:: ${payload}` 23 | ); 24 | }; 25 | logger.error = (msg: string | {}) => logger(msg, 'error'); 26 | 27 | logger('Initializing Tamagui Plugin...'); 28 | 29 | const ctxBase: TSContextBase = { 30 | info, 31 | modules, 32 | logger, 33 | }; 34 | 35 | const getContext = (): TSContext | TSContextBase => { 36 | const program = info.project['program'] as ts.Program | undefined; 37 | if (!program) return ctxBase; 38 | return { 39 | ...ctxBase, 40 | program, 41 | typeChecker: program.getTypeChecker(), 42 | }; 43 | }; 44 | 45 | const options = readOptions(ctxBase); 46 | 47 | logger(`Options parsed`); 48 | logger(options); 49 | 50 | const tamaguiConfig = readConfig(options, ctxBase); 51 | 52 | if (!tamaguiConfig) { 53 | logger.error(`Tamagui config was not parsed.`); 54 | return info.languageService; 55 | } 56 | 57 | const languageServerHooks = getLanguageServerHooks({ 58 | config: tamaguiConfig, 59 | options, 60 | getContext, 61 | }); 62 | 63 | const proxy: ts.LanguageService = Object.create(null); 64 | for (const k of Object.keys(info.languageService) as Array< 65 | keyof ts.LanguageService 66 | >) { 67 | const x = info.languageService[k]!; 68 | // @ts-expect-error - this is fine 69 | proxy[k] = (...args: Array<{}>) => 70 | // @ts-expect-error - this is fine 71 | x.apply(info.languageService, args); 72 | } 73 | 74 | Object.assign(proxy, languageServerHooks); 75 | 76 | const { tamaguiConfigFilePath } = options; 77 | 78 | logger(`Setting up watcher for <${tamaguiConfigFilePath}>`); 79 | 80 | const watchHost = modules.typescript.createWatchCompilerHost( 81 | [tamaguiConfigFilePath], 82 | { resolveJsonModule: true }, 83 | modules.typescript.sys 84 | ); 85 | 86 | watchHost.watchFile(tamaguiConfigFilePath, () => { 87 | logger('tamagui.config.json was updated.'); 88 | const nextTamaguiConfig = readConfig(options, getContext()); 89 | if (!nextTamaguiConfig) { 90 | logger(`Failed to parse updated tamagui config.`); 91 | return; 92 | } 93 | logger('Replacing tamagui config with new values.'); 94 | for (const key in Object.keys(tamaguiConfig)) { 95 | delete tamaguiConfig[key as keyof typeof tamaguiConfig]; 96 | } 97 | Object.assign(tamaguiConfig, nextTamaguiConfig); 98 | }); 99 | 100 | const _watchProgram = modules.typescript.createWatchProgram(watchHost); 101 | 102 | return proxy; 103 | }, 104 | }; 105 | 106 | return plugin; 107 | }; 108 | 109 | export = init; 110 | -------------------------------------------------------------------------------- /src/mapPropToToken.ts: -------------------------------------------------------------------------------- 1 | import { tokenCategories } from '@tamagui/helpers'; 2 | 3 | import { ParsedConfig } from './readConfig'; 4 | 5 | interface MapPropToTokenReturn { 6 | type: Exclude; 7 | shorthand?: string; 8 | prop: string; 9 | isShorthand: boolean; 10 | } 11 | 12 | /** 13 | * Maps a property or shorthand to a token category 14 | */ 15 | export const mapPropToToken = ( 16 | prop: string, 17 | config: ParsedConfig 18 | ): MapPropToTokenReturn => { 19 | const shorthandMapped = config.shorthands[ 20 | prop as keyof typeof config.shorthands 21 | ] as string | undefined; 22 | const isShorthand = Boolean(shorthandMapped); 23 | const realProp = shorthandMapped ?? prop; 24 | 25 | for (const [category, properties] of Object.entries(tokenCategories)) { 26 | if (realProp in properties) { 27 | return { 28 | type: category as Exclude< 29 | keyof ParsedConfig, 30 | 'shorthands' | 'themeColors' 31 | >, 32 | shorthand: isShorthand ? prop : undefined, 33 | prop: realProp, 34 | isShorthand, 35 | }; 36 | } 37 | } 38 | 39 | return { type: 'space', shorthand: prop, prop: realProp, isShorthand }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/metadata.ts: -------------------------------------------------------------------------------- 1 | import color from 'color'; 2 | 3 | import { PluginOptions } from './readOptions'; 4 | import { toPascal } from './utils'; 5 | 6 | const squirclePath = `M 0,12 C 0,0 0,0 12,0 24,0 24,0 24,12 24,24 24,24 12,24 0, 24 0,24 0,12`; 7 | 8 | const svgCheckerboard = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | `; 17 | 18 | const makeColorTile = (value: string, size: number) => { 19 | try { 20 | const colorValue = color(value); 21 | const hasAlphaTransparency = colorValue.alpha() !== 1; 22 | const svg = `${ 23 | hasAlphaTransparency ? svgCheckerboard : '' 24 | }`; 25 | const image = `![Image](data:image/svg+xml;base64,${btoa(svg)})`; 26 | return image; 27 | } catch { 28 | return ''; 29 | } 30 | }; 31 | 32 | const makeTable = (rows: Record[]) => { 33 | const header = rows[0]!; 34 | const keys = Object.keys(header); 35 | const renderRow = (row: Record) => { 36 | return `| ${keys.map((key) => row[key]).join(' | ')} |`; 37 | }; 38 | const renderSplitter = () => { 39 | return `| ${keys.map(() => '---').join(' | ')} |`; 40 | }; 41 | 42 | return `${renderRow(header)}\n${renderSplitter()}\n${rows 43 | .slice(1) 44 | .map(renderRow) 45 | .join('\n')}`; 46 | }; 47 | 48 | export const makeTokenDescription = (scale: string, value: string) => { 49 | return makeTable([ 50 | { scale: 'Scale', value: 'Value' }, 51 | { 52 | scale: `**${scale}**`, 53 | value, 54 | }, 55 | ]); 56 | }; 57 | 58 | export const makeColorTokenDescription = ( 59 | value: string, 60 | options: PluginOptions 61 | ) => { 62 | return makeTable([ 63 | { color: 'Color', value: 'Value' }, 64 | { 65 | color: makeColorTile(value, options.colorTileSize), 66 | value: `\`${value}\``, 67 | }, 68 | ]); 69 | }; 70 | 71 | export const makeShorthandDescription = (shorthand: string, prop: string) => { 72 | return `\`${shorthand}\` is shorthand for \`${prop}\``; 73 | }; 74 | 75 | const formatThemePrefix = (key: string) => { 76 | return key.replace(/([A-Za-z0-9]+)(?:_|$)/g, (_, key) => toPascal(key)); 77 | }; 78 | 79 | export const makeThemeTokenDescription = ( 80 | values: Record, 81 | options: PluginOptions 82 | ) => { 83 | const table = [{ color: ' ', theme: 'Theme', value: 'Value' }]; 84 | 85 | let groupPrefix = ''; 86 | for (const [themeKey, value] of Object.entries(values)) { 87 | if (!themeKey.includes('_')) { 88 | table.push({ 89 | color: makeColorTile(value, options.colorTileSize), 90 | theme: `**${toPascal(themeKey)}**`, 91 | value: `\`${value}\``, 92 | }); 93 | } else { 94 | const [, group, key] = themeKey.match(/((?:[A-Za-z0-9]+_)+)(.+)/) ?? []; 95 | if (!group || !key) { 96 | throw new Error(`TSTamagui:: Invalid theme key <${themeKey}>`); 97 | } 98 | if (group !== groupPrefix) { 99 | groupPrefix = group; 100 | table.push({ 101 | color: ' ', 102 | theme: `┌\u{A0}**${formatThemePrefix(group)}**`, 103 | value: '──────', 104 | }); 105 | } 106 | table.push({ 107 | color: `${makeColorTile(value, options.colorTileSize)}`, 108 | theme: `├ **${toPascal(key)}**`, 109 | value: `\`${value}\``, 110 | }); 111 | } 112 | } 113 | 114 | return makeTable(table); 115 | }; 116 | -------------------------------------------------------------------------------- /src/readConfig.ts: -------------------------------------------------------------------------------- 1 | import type { TamaguiInternalConfig } from '@tamagui/core'; 2 | 3 | import { PluginOptions } from './readOptions'; 4 | import { TSContextBase } from './types'; 5 | 6 | type Themes = TamaguiInternalConfig['themes']; 7 | 8 | type ThemeKey = keyof TamaguiInternalConfig['themes'] & string; 9 | 10 | type Theme = Themes[ThemeKey]; 11 | 12 | /** 13 | * Simple utility to transform a token map into a simple key-value map 14 | */ 15 | const simplifyTokenMap = ( 16 | tokens: TamaguiInternalConfig['tokens'][keyof TamaguiInternalConfig['tokens']], 17 | transformNumbersToPx = true 18 | ) => { 19 | return Object.fromEntries( 20 | Object.values(tokens).map((variable) => { 21 | return [ 22 | variable.key as string, 23 | transformNumbersToPx && typeof variable.val === 'number' 24 | ? `${variable.val}px` 25 | : `${variable.val}`, 26 | ]; 27 | }) 28 | ) as Record; 29 | }; 30 | 31 | // Ignore pattern for component themes 32 | const componentThemePattern = /_[A-Z]/; 33 | 34 | /** 35 | * Get count of underscores in a string 36 | */ 37 | const underscoreDepth = (str: string) => str.split('_').length - 1; 38 | 39 | /** 40 | * Sort themes by depth of underscores, then by presence of defaultTheme, 41 | * then alphabetically 42 | */ 43 | const sortThemes = (defaultTheme: string) => { 44 | const defaultThemePrefix = `${defaultTheme}_`; 45 | return ([keyA]: [ThemeKey, Theme], [keyB]: [ThemeKey, Theme]) => { 46 | const depthA = underscoreDepth(keyA); 47 | const depthB = underscoreDepth(keyB); 48 | if (depthA === depthB) { 49 | const isADefault = keyA === defaultTheme; 50 | const isADefaultSubtheme = keyA.startsWith(defaultThemePrefix); 51 | const isBDefault = keyB === defaultTheme; 52 | const isBDefaultSubtheme = keyB.startsWith(defaultThemePrefix); 53 | if (isADefault) return -1; 54 | if (isBDefault) return 1; 55 | if (isADefaultSubtheme && !isBDefaultSubtheme) return -1; 56 | if (isBDefaultSubtheme && !isADefaultSubtheme) return 1; 57 | return keyA.localeCompare(keyB); 58 | } 59 | return depthA - depthB; 60 | }; 61 | }; 62 | 63 | /** 64 | * Utility to transform a record of themes into a record of theme tokens 65 | * with the theme names as keys and the token values as values 66 | */ 67 | const getThemeColors = ( 68 | themes: TamaguiInternalConfig['themes'], 69 | { defaultTheme }: PluginOptions 70 | ) => { 71 | const themeTokens: Record> = {}; 72 | 73 | const sortedThemes = Object.entries(themes); 74 | sortedThemes.sort(sortThemes(defaultTheme)); 75 | 76 | for (const [themeName, theme] of sortedThemes) { 77 | if (componentThemePattern.test(themeName)) continue; 78 | 79 | for (const [key, variable] of Object.entries(theme)) { 80 | if (key === 'id') continue; 81 | const $key = `$${key}`; 82 | themeTokens[$key] ??= {}; 83 | themeTokens[$key]![themeName as string] = (variable.val ?? 84 | variable) as unknown as string; 85 | } 86 | } 87 | 88 | return themeTokens; 89 | }; 90 | 91 | /** 92 | * Read and process the tamagui config file into a simpler format 93 | */ 94 | export const readConfig = ( 95 | options: PluginOptions, 96 | { modules, logger }: TSContextBase 97 | ) => { 98 | const { tamaguiConfigFilePath } = options; 99 | const tamaguiConfigFile = modules.typescript.sys.readFile( 100 | tamaguiConfigFilePath 101 | ); 102 | 103 | if (!tamaguiConfigFile) { 104 | logger.error(`No tamagui config file found.`); 105 | return undefined; 106 | } 107 | 108 | try { 109 | const jsonConfig = JSON.parse(tamaguiConfigFile) 110 | .tamaguiConfig as TamaguiInternalConfig; 111 | const { 112 | // TODO: 113 | // fontSizeTokens, 114 | shorthands, 115 | tokens, 116 | themes, 117 | } = jsonConfig; 118 | 119 | const color = simplifyTokenMap(tokens.color); 120 | const space = simplifyTokenMap(tokens.space); 121 | const size = simplifyTokenMap(tokens.size); 122 | const radius = simplifyTokenMap(tokens.radius); 123 | const zIndex = simplifyTokenMap(tokens.zIndex, false); 124 | const themeColors = getThemeColors(themes, options); 125 | 126 | const config = { 127 | color, 128 | space, 129 | size, 130 | radius, 131 | shorthands, 132 | themeColors, 133 | zIndex, 134 | } as const; 135 | 136 | logger('Tamagui config parsed!'); 137 | logger(config); 138 | 139 | return config; 140 | } catch (e) { 141 | logger.error( 142 | `Failed to parse tamagui config: ${ 143 | e instanceof Error ? e.message : '(unknown)' 144 | }` 145 | ); 146 | return undefined; 147 | } 148 | }; 149 | 150 | export type ParsedConfig = Exclude, undefined>; 151 | -------------------------------------------------------------------------------- /src/readOptions.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { TSContextBase } from './types'; 4 | 5 | const normalizeFilter = (token: string) => 6 | token.startsWith('$') ? token : `$${token}`; 7 | 8 | export const createCustomTokenFilter = ({ 9 | themeColor, 10 | color, 11 | size, 12 | space, 13 | radius, 14 | zIndex, 15 | }: { 16 | themeColor?: string[]; 17 | color?: string[]; 18 | size?: string[]; 19 | space?: string[]; 20 | radius?: string[]; 21 | zIndex?: string[]; 22 | }) => { 23 | if (!themeColor && !color && !size && !space && !radius && !zIndex) 24 | return undefined; 25 | 26 | const customFilters = { 27 | themeColor: themeColor && new Set(themeColor?.map(normalizeFilter)), 28 | color: color && new Set(color?.map(normalizeFilter)), 29 | size: size && new Set(size?.map(normalizeFilter)), 30 | space: space && new Set(space?.map(normalizeFilter)), 31 | radius: radius && new Set(radius?.map(normalizeFilter)), 32 | zIndex: zIndex && new Set(zIndex?.map(normalizeFilter)), 33 | } as const; 34 | 35 | const customFilter = (scale: keyof typeof customFilters, token: string) => { 36 | return customFilters?.[scale]?.has(token); 37 | }; 38 | 39 | customFilter.toString = () => 40 | Object.entries(customFilters) 41 | .filter(([, val]) => !!val) 42 | .map(([key, val]) => `${key}<${[...val!].join(', ')}>`) 43 | .join('; '); 44 | 45 | return customFilter; 46 | }; 47 | 48 | /** 49 | * Read options passed in from the tsconfig.json file 50 | */ 51 | export const readOptions = ({ info, modules }: TSContextBase) => { 52 | const { 53 | pathToApp = 'apps/next', 54 | defaultTheme = 'light', 55 | colorTileSize = 18, 56 | showShorthandTranslations = true, 57 | completionFilters: { 58 | showColorTokens = true, 59 | showTrueTokens = true, 60 | custom: { 61 | themeColor: themeColorFilters = undefined, 62 | color: colorFilters = undefined, 63 | size: sizeFilters = undefined, 64 | space: spaceFilters = undefined, 65 | radius: radiusFilters = undefined, 66 | zIndex: zIndexFilters = undefined, 67 | } = {}, 68 | } = {}, 69 | } = info.config as { 70 | pathToApp?: string; 71 | defaultTheme?: string; 72 | colorTileSize?: number; 73 | showShorthandTranslations?: boolean; 74 | completionFilters?: { 75 | showColorTokens?: boolean; 76 | showTrueTokens?: boolean; 77 | custom?: { 78 | themeColor?: string[]; 79 | color?: string[]; 80 | size?: string[]; 81 | space?: string[]; 82 | radius?: string[]; 83 | zIndex?: string[]; 84 | }; 85 | }; 86 | }; 87 | 88 | const rootDir = path.join( 89 | modules.typescript.sys.getExecutingFilePath(), 90 | '../../../../' 91 | ); 92 | 93 | const tamaguiConfigFilePath = path.join( 94 | path.isAbsolute(pathToApp) ? pathToApp : path.join(rootDir, pathToApp), 95 | './.tamagui/tamagui.config.json' 96 | ); 97 | 98 | return { 99 | tamaguiConfigFilePath, 100 | defaultTheme, 101 | colorTileSize, 102 | showShorthandTranslations, 103 | completionFilters: { 104 | showColorTokens, 105 | showTrueTokens, 106 | custom: createCustomTokenFilter({ 107 | themeColor: themeColorFilters, 108 | color: colorFilters, 109 | size: sizeFilters, 110 | space: spaceFilters, 111 | radius: radiusFilters, 112 | zIndex: zIndexFilters, 113 | }), 114 | }, 115 | } as const; 116 | }; 117 | 118 | export type PluginOptions = ReturnType; 119 | -------------------------------------------------------------------------------- /src/safeInsertDocs.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | type ResultWithDocumentation = { 4 | documentation?: ts.SymbolDisplayPart[]; 5 | }; 6 | 7 | /** 8 | * Safely inserts documentation into a result with deduping 9 | * 10 | * Pass null as the value to remove the documentation if it exists 11 | */ 12 | export const safeInsertDocs = ( 13 | target: T, 14 | id: string, 15 | value: string | null 16 | ): void => { 17 | const idTag = `\n`; 18 | if (target.documentation) { 19 | const existingTagIndex = target.documentation.findIndex((doc) => 20 | doc.text.endsWith(idTag) 21 | ); 22 | if (existingTagIndex !== -1) { 23 | target.documentation.splice(existingTagIndex, 1); 24 | } 25 | } 26 | if (value === null) { 27 | return; 28 | } 29 | target.documentation ??= []; 30 | target.documentation.unshift({ 31 | kind: 'markdown', 32 | text: `${value}\n\n${idTag}`, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | export type tss = typeof ts; 4 | 5 | export type TSContextBase = { 6 | info: ts.server.PluginCreateInfo; 7 | modules: { typescript: tss }; 8 | logger: ((msg: string | {}, type?: 'info' | 'error') => void) & { 9 | error: (msg: string | {}) => void; 10 | }; 11 | }; 12 | 13 | export type TSContext = TSContextBase & { 14 | program: ts.Program; 15 | typeChecker: ts.TypeChecker; 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ParsedConfig } from './readConfig'; 2 | 3 | // Detect specific scale tokens 4 | const specificTokenPattern = /^\$(color|radius|size|space|zIndex)\.(.+)$/; 5 | 6 | /** 7 | * Reformats a token string to be used for sorting 8 | * 9 | * - Specific tokens are sorted last 10 | * - Numeric portions of tokens are sorted numerically 11 | */ 12 | export const getSortText = (sortText: string) => { 13 | let text = sortText; 14 | // add prefix to specific tokens to sort them last: 15 | text = text.replace(specificTokenPattern, '$zzzzzzzzzz$1.$2'); 16 | // add prefix to numeric portions of tokens to sort them numerically: 17 | text = text.replace(/(-?)(\d+)/g, (_, sign, num) => { 18 | return `${sign ? '1' : '0'}${num.padStart(12, '0')}`; 19 | }); 20 | // special case for negative text tokens like $-true 21 | text = text.replace(/(^\$|\.)-(\w.+)$/, '$1z$2'); 22 | return text; 23 | }; 24 | 25 | /** 26 | * Extracts the token and scale from a token string 27 | * If a specific token was not used, the default scale is returneds 28 | */ 29 | export const getMaybeSpecificToken = ( 30 | entryName: string, 31 | defaultScale: keyof ParsedConfig, 32 | config: ParsedConfig 33 | ) => { 34 | const sanitizedEntryName = sanitizeMaybeQuotedString(entryName); 35 | const [, scale, token] = sanitizedEntryName.match(specificTokenPattern) ?? [ 36 | null, 37 | defaultScale, 38 | sanitizedEntryName, 39 | ]; 40 | 41 | if (!scale || !token) return [undefined, undefined] as const; 42 | 43 | const scaleObj = config[scale as keyof ParsedConfig] as 44 | | ParsedConfig[keyof ParsedConfig] 45 | | undefined; 46 | 47 | const tokenKey = ( 48 | token.startsWith('$') ? token : `$${token}` 49 | ) as keyof typeof scaleObj; 50 | const val = scaleObj?.[tokenKey] as string | undefined; 51 | 52 | if (!val) return [undefined, undefined] as const; 53 | 54 | return [ 55 | scale as Exclude, 56 | val, 57 | tokenKey, 58 | ] as const; 59 | }; 60 | 61 | /** 62 | * Removes quotes from a string if it's quoted 63 | */ 64 | export const sanitizeMaybeQuotedString = (str: string) => 65 | str.replace(/^['"]|['"]$/g, ''); 66 | 67 | // TODO: should be more robust 68 | export const toPascal = (str: string) => { 69 | if (str.length < 1) return ''; 70 | return str.charAt(0).toUpperCase() + str.slice(1); 71 | }; 72 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es2019", 5 | "module": "CommonJS", 6 | "sourceMap": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "noUncheckedIndexedAccess": true, 12 | "skipLibCheck": true, 13 | "rootDir": "./src" 14 | }, 15 | "include": ["src"] 16 | } 17 | --------------------------------------------------------------------------------