diff --git a/README.md b/README.md index 3ccfa4a..022b38c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Provides editor support for ```tw`...```` tagged template syntax including: - Autocomplete for [twind](https://github.com/tw-in-js/twind) classes - Warnings on unknown classes - Warnings on unknown theme values +- Warnings on unknown variants ## Installation @@ -47,6 +48,30 @@ npm install --save-dev typescript @twind/typescript-plugin This plugin requires TypeScript 4.1 or later. It can provide intellisense in both JavaScript and TypeScript files within any editor that uses TypeScript to power their language features. This includes [VS Code](https://code.visualstudio.com), [Sublime with the TypeScript plugin](https://github.com/Microsoft/TypeScript-Sublime-Plugin), [Atom with the TypeScript plugin](https://atom.io/packages/atom-typescript), [Visual Studio](https://www.visualstudio.com), and others. +If you have a custom twind configuration you need to extract that into an own file. Create a `twind.config.{ts,js,cjs,mjs}` in your root folder. Here is using a custom plugin: + +```js +import { forms, formInput } from '@twind/forms' + +/** @type {import('twind').Configuration} */ +export default { + plugins: { forms, 'form-input': formInput} +} + +declare module 'twind' { + interface Plugins { + // forms should have been detected from setup – not need to add it + // forms: '' + + // We want to add sm and lg modifiers to the form-input + 'form-input': + | '' // plain form-input + | 'sm' // form-input-sm + | 'lg' // form-input-lg + } +} +``` + ### With VS Code Currently you must manually install the plugin along side TypeScript in your workspace. diff --git a/package.json b/package.json index a2615d0..b042a9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@twind/typescript-plugin", - "version": "0.2.0", + "version": "0.2.1", "description": "TypeScript language service plugin that adds IntelliSense for twind", "//": "mark as private to prevent accidental publish - use 'yarn release'", "private": true, @@ -30,7 +30,10 @@ "build": "distilt", "format": "prettier --write --ignore-path .gitignore .", "lint": "eslint --ext .js,.ts --ignore-path .gitignore .", + "postlint": "tsc", "lint:fix": "yarn lint -- --fix", + "test": "uvu -r esm -r test-env.js", + "test:watch": "watchlist src -- yarn test", "release": "npx np --contents dist", "version": "yarn build" }, @@ -108,11 +111,15 @@ "@typescript-eslint/eslint-plugin": "^4.9.1", "@typescript-eslint/parser": "^4.9.1", "distilt": "^0.10.1", + "esbuild-register": "^2.3.0", "eslint": "^7.15.0", "eslint-config-prettier": "^7.0.0", "eslint-plugin-prettier": "^3.2.0", + "esm": "^3.2.25", "execa": "^5.0.0", - "prettier": "^2.0.5" + "prettier": "^2.0.5", + "uvu": "^0.5.1", + "watchlist": "^0.2.3" }, "publishConfig": { "access": "public", diff --git a/src/language-service.ts b/src/language-service.ts index d6976fc..a65e3f1 100644 --- a/src/language-service.ts +++ b/src/language-service.ts @@ -13,8 +13,8 @@ import type { ConfigurationManager } from './configuration' import { defaultBaseSortFn, matchSorter } from 'match-sorter' -import { parse, ParsedRule } from './parse' import { CompletionToken, Twind } from './twind' +import { parse, Rule } from './parser' function arePositionsEqual(left: ts.LineAndCharacter, right: ts.LineAndCharacter): boolean { return left.line === right.line && left.character === right.character @@ -35,7 +35,8 @@ function arePositionsEqual(left: ts.LineAndCharacter, right: ts.LineAndCharacter const enum ErrorCodes { UNKNOWN_DIRECTIVE = -2020, - UNKNOWN_THEME_VALUE = -2021, + UNKNOWN_VARIANT = -2021, + UNKNOWN_THEME_VALUE = -2022, } const pad = (n: string): string => n.padStart(8, '0') @@ -121,124 +122,107 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { ): ts.CompletionEntryDetails { const item = this.getCompletionItems(context, position).items.find((x) => x.label === name) - if (!item) { - return { - name, - kind: this.typescript.ScriptElementKind.unknown, - kindModifiers: '', - tags: [], - displayParts: toDisplayParts(name), - documentation: [], - } + if (item) { + return translateCompletionItemsToCompletionEntryDetails(this.typescript, item) } - return translateCompletionItemsToCompletionEntryDetails(this.typescript, item) + return { + name, + kind: this.typescript.ScriptElementKind.unknown, + kindModifiers: '', + tags: [], + displayParts: toDisplayParts(name), + documentation: [], + } } public getQuickInfoAtPosition( context: TemplateContext, position: ts.LineAndCharacter, ): ts.QuickInfo | undefined { - const offset = context.toOffset(position) - - const find = (char: string): number => { - const index = context.text.indexOf(char, offset) - return index >= offset ? index : context.text.length - } + const rules = parse(context.text, context.toOffset(position)) - const nextBoundary = Math.min(find(')'), find(' '), find('\t'), find('\n'), find('\r')) + const rule = rules.map((rule) => rule.value).join(' ') - if (nextBoundary == offset) { + if (!rule) { return undefined } - const parsed = parse(context.text, nextBoundary) - - const end = parsed.tokenStartOffset + (parsed.token == '&' ? -1 : 0) - const start = Math.max(0, end - parsed.token.length) - const rule = parsed.prefix ? parsed.rule.replace('&', parsed.prefix) : parsed.rule - const css = this._twind.css(rule) - if (css) { - return { - kind: translateCompletionItemKind(this.typescript, vscode.CompletionItemKind.Property), - kindModifiers: '', - textSpan: { - start, - length: end - start, - }, - displayParts: toDisplayParts(rule), - // displayParts: [], - documentation: toDisplayParts({ - kind: 'markdown', - value: '```css\n' + css + '\n```', - }), - tags: [], - } - } + const span = rules + .flatMap((rule) => rule.spans) + .reduce((a, b) => ({ + start: Math.min(a.start, b.start), + end: Math.max(a.end, b.end), + })) - return undefined + return { + kind: translateCompletionItemKind(this.typescript, vscode.CompletionItemKind.Property), + kindModifiers: '', + textSpan: { + start: span.start, + length: span.end - span.start, + }, + displayParts: toDisplayParts(rule), + documentation: css + ? toDisplayParts({ + kind: 'markdown', + value: '```css\n' + css + '\n```', + }) + : undefined, + tags: [], + } } public getSemanticDiagnostics(context: TemplateContext): ts.Diagnostic[] { - const diagnostics: ts.Diagnostic[] = [] - - const { text } = context - - for (let offset = 0; offset <= text.length; offset++) { - if (') \t\n\r'.includes(text[offset]) || offset == text.length) { - const parsed = parse(context.text, offset) - - const rule = parsed.prefix ? parsed.rule.replace('&', parsed.prefix) : parsed.rule - - if (rule) { - const end = offset - const start = Math.max(0, end - parsed.token.length) - - this.logger.log( - `getDiagnostics: ${rule} ${parsed.token} - ${parsed.tokenStartOffset} ${start}:${end}`, - ) - - this._twind.getDiagnostics(rule)?.some((info) => { - switch (info.id) { - case 'UNKNOWN_DIRECTIVE': { - diagnostics.push({ - messageText: `Unknown utility "${ - parsed.prefix ? parsed.directive.replace('&', parsed.prefix) : parsed.directive - }"`, - start: start, - length: end - start, + return parse(context.text).flatMap((rule): ts.Diagnostic[] => + (this._twind.getDiagnostics(rule.value) || []) + .map((info): ts.Diagnostic | undefined => { + switch (info.id) { + case 'UNKNOWN_DIRECTIVE': { + return { + messageText: `Unknown utility "${rule.name}"`, + start: rule.loc.start, + length: rule.loc.end - rule.loc.start, + file: context.node.getSourceFile(), + category: this.typescript.DiagnosticCategory.Warning, + code: ErrorCodes.UNKNOWN_DIRECTIVE, + } + } + case 'UNKNOWN_THEME_VALUE': { + if (info.key) { + const [section, ...key] = info.key?.split('.') + + return { + messageText: `Unknown theme value "${section}[${key.join('.')}]"`, + start: rule.loc.start, + length: rule.loc.end - rule.loc.start, file: context.node.getSourceFile(), category: this.typescript.DiagnosticCategory.Warning, - code: ErrorCodes.UNKNOWN_DIRECTIVE, - }) - return true - } - case 'UNKNOWN_THEME_VALUE': { - if (info.key) { - const [section, ...key] = info.key?.split('.') - - diagnostics.push({ - messageText: `Unknown theme value "${section}[${key.join('.')}]"`, - start: start, - length: end - start, - file: context.node.getSourceFile(), - category: this.typescript.DiagnosticCategory.Warning, - code: ErrorCodes.UNKNOWN_THEME_VALUE, - }) - return true + code: ErrorCodes.UNKNOWN_THEME_VALUE, } } } - - return false - }) - } - } - } - - return diagnostics + } + }) + // check if every rule.variants exist + .concat( + rule.variants + .filter((variant) => !this._twind.completions.variants.has(variant.raw)) + .map( + (variant): ts.Diagnostic => ({ + messageText: `Unknown variant "${variant.raw}"`, + start: variant.loc.start, + length: variant.loc.end - variant.loc.start, + file: context.node.getSourceFile(), + category: this.typescript.DiagnosticCategory.Warning, + code: ErrorCodes.UNKNOWN_VARIANT, + }), + ), + ) + .filter((value): value is ts.Diagnostic => Boolean(value)), + ) } // public getSupportedCodeFixes(): number[] { @@ -284,24 +268,24 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { context: TemplateContext, position: ts.LineAndCharacter, completion: CompletionToken, - parsed: ParsedRule, + rule: Rule, sortedIndex: number, ): vscode.CompletionItem { const label = - parsed.prefix && completion.kind == 'utility' - ? completion.label.slice(parsed.prefix.length + 1) + rule.prefix && completion.kind == 'utility' + ? completion.label.slice(rule.prefix.length + 1) : completion.label const newText = - parsed.prefix && completion.kind == 'utility' - ? completion.value.slice(parsed.prefix.length + 1) + rule.prefix && completion.kind == 'utility' + ? completion.value.slice(rule.prefix.length + 1) : completion.value const textEdit = { newText, range: { - start: context.toPosition(Math.max(0, parsed.tokenStartOffset - parsed.token.length)), - end: context.toPosition(parsed.tokenStartOffset), + start: context.toPosition(rule.loc.start), + end: context.toPosition(rule.loc.end), }, } @@ -316,7 +300,7 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { data: completion.kind, label, preselect: false, - filterText: parsed.rule, + filterText: rule.value, sortText: sortedIndex.toString().padStart(8, '0'), detail: completion.detail, documentation: { @@ -339,7 +323,7 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { '**Parsed**\n\n```json\n' + JSON.stringify( { - ...parsed, + ...rule, sortedIndex, textEdit, }, @@ -381,33 +365,38 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { const { completions: twindCompletions } = this._twind - const parsed = parse(context.text, context.toOffset(position)) + const rule = parse(context.text, context.toOffset(position), true) - const hasScreenVariant = parsed.variants.some((x) => twindCompletions.screens.includes(x)) + const hasScreenVariant = rule.variants.some((variant) => + twindCompletions.screens.has(variant.raw), + ) const screens = hasScreenVariant ? [] : twindCompletions.tokens.filter((completion) => completion.kind == 'screen') const variants = twindCompletions.tokens.filter( - (completion) => completion.kind == 'variant' && !parsed.variants.includes(completion.value), + (completion) => + completion.kind == 'variant' && + !rule.variants.some((variant) => variant.raw === completion.value), ) const utilities = twindCompletions.tokens.filter( (completion) => - (completion.kind == 'utility' && !parsed.prefix) || - completion.value.startsWith(parsed.prefix + '-'), + completion.kind == 'utility' && + (!rule.negated || completion.value.startsWith('-')) && + (!rule.prefix || completion.value.startsWith(rule.prefix + '-')), ) // TODO Start a new directive group const matched = [ - ...matchSorter(screens, prepareText(parsed.token), { + ...matchSorter(screens, prepareText(rule.raw), { threshold: matchSorter.rankings.MATCHES, keys: [ (completion) => prepareText(naturalExpand(completion.detail) + ' ' + completion.value), ], }), - ...matchSorter(utilities, prepareText(parsed.directive), { + ...matchSorter(utilities, prepareText(rule.raw), { threshold: matchSorter.rankings.ACRONYM, keys: [(completion) => prepareText(naturalExpand(completion.value))], sorter: (items) => @@ -423,14 +412,14 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { return defaultBaseSortFn(a, b) }), }), - ...matchSorter(variants, prepareText(parsed.token), { + ...matchSorter(variants, prepareText(rule.raw), { threshold: matchSorter.rankings.ACRONYM, keys: [(completion) => prepareText(completion.value)], }), ] completions.items = matched.map((completion, index) => - this.getCompletionItem(context, position, completion, parsed, index), + this.getCompletionItem(context, position, completion, rule, index), ) // this._completions.tokens.forEach((completion) => { @@ -485,12 +474,12 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { // })), // ] - if (parsed.prefix && (!parsed.token || parsed.token === '&')) { + if (rule.prefix && (!rule.raw || rule.raw === '&')) { completions.items.unshift({ kind: vscode.CompletionItemKind.Constant, label: `&`, - detail: `${parsed.prefix}`, - sortText: `&${parsed.prefix}`, + detail: `${rule.prefix}`, + sortText: `&${rule.prefix}`, documentation: this.configurationManager.config.debug ? { kind: vscode.MarkupKind.Markdown, @@ -498,7 +487,7 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { '**Parsed**\n\n```json\n' + JSON.stringify( { - ...parsed, + ...rule, items: completions.items.map(({ label, filterText, sortText, textEdit }) => ({ label, filterText, @@ -518,10 +507,10 @@ export class TwindTemplateLanguageService implements TemplateLanguageService { } : undefined, textEdit: { - newText: `&`.slice(parsed.token.length), + newText: '&', range: { - start: context.toPosition(Math.max(0, parsed.tokenStartOffset - parsed.token.length)), - end: context.toPosition(parsed.tokenStartOffset), + start: context.toPosition(rule.loc.start), + end: context.toPosition(rule.loc.end), }, }, }) diff --git a/src/parse.ts b/src/parse.ts deleted file mode 100644 index 24a7a81..0000000 --- a/src/parse.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Shared variables used during parsing - -// List of active groupings: either variant ('xxx:') or prefix -const groupings: string[] = [] - -// List of parsed rules -export interface ParsedRule { - token: string - - /** [":sm", ":dark", ":hover"] */ - variants: string[] - - important: boolean - - prefix: string - - /** "text-sm", "rotate-45" */ - directive: string - - rule: string - - tokenStartOffset: number -} - -const startGrouping = (value = ''): '' => { - groupings.push(value) - return '' -} - -const endGrouping = (isWhitespace?: boolean): void => { - // If isWhitespace is true - // ['', ':sm', ':hover'] => [''] - // ['', ':sm', ':hover', ''] => ['', ':sm', ':hover'] - - // If isWhitespace is falsey - // ['', ':sm', ':hover'] => [''] - // ['', ':sm', ':hover', ''] => ['', ':sm', ':hover', ''] - - groupings.length = Math.max(groupings.lastIndexOf('') + ~~(isWhitespace as boolean), 0) -} - -const onlyPrefixes = (s: string): '' | boolean => s && s[0] !== ':' -const onlyVariants = (s: string): '' | boolean => s[0] === ':' - -export const parse = (text: string, offset = text.length): ParsedRule => { - groupings.length = 0 - - let char: string - let token = '' - let tokenStartOffset = 0 - - for (let position = 0; position < offset; position++) { - switch ((char = text[position])) { - case ':': - if (token) { - tokenStartOffset = offset - token = startGrouping(':' + (text[position + 1] == char ? text[position++] : '') + token) - } - - break - - case '(': - // If there is a token this is the prefix for all grouped tokens - if (token) { - tokenStartOffset = offset - token = startGrouping(token) - } - - startGrouping() - - break - - case ')': - case ' ': - case '\t': - case '\n': - case '\r': - tokenStartOffset = offset - token = '' - endGrouping(char !== ')') - - break - - default: - token += char - } - } - - const variants = groupings.filter(onlyVariants).map((variant) => { - if (variant.startsWith('::')) { - return variant.slice(2) + '::' - } - - return variant.slice(1) + ':' - }) - const prefix = groupings.filter(onlyPrefixes).join('-') - const directive = token === '&' ? token : (prefix && prefix + '-') + token - - if (token === '&') { - tokenStartOffset += 1 - } - - const important = token[token.length - 1] == '!' - - if (important) { - token = token.slice(0, -1) - } - - return { - token, - variants, - important, - prefix, - directive, - rule: variants.join('') + directive, - tokenStartOffset, - } -} diff --git a/src/parser.test.ts b/src/parser.test.ts new file mode 100644 index 0000000..691124e --- /dev/null +++ b/src/parser.test.ts @@ -0,0 +1,778 @@ +// prose prose-xl lg:prose-xl +// prose(& xl:& lg:xl) +// ring(&(&)) +import { suite } from 'uvu' +import * as assert from 'uvu/assert' + +import { parse } from './parser' + +const test = suite('Parser') + +/** + * Get all rules + */ +;([ + ['', []], + [' \t \n\r ', []], + [ + 'underline', + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'hover:underline', + [ + { + raw: 'underline', + value: 'hover:underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 6, end: 15 }, + spans: [{ start: 0, end: 15 }], + variants: [{ name: 'hover', raw: 'hover:', loc: { start: 0, end: 6 } }], + }, + ], + ], + [ + '-mx-5!', + [ + { + raw: '-mx-5!', + value: '-mx-5!', + name: 'mx-5', + prefix: '', + important: true, + negated: true, + loc: { start: 0, end: 6 }, + spans: [{ start: 0, end: 6 }], + variants: [], + }, + ], + ], + [ + '-mx(5 sm:2! xl:8)', + [ + { + raw: '5', + value: '-mx-5', + name: 'mx-5', + prefix: '-mx', + important: false, + negated: true, + loc: { start: 4, end: 5 }, + spans: [ + { start: 0, end: 3 }, + { start: 4, end: 5 }, + ], + variants: [], + }, + { + raw: '2!', + value: 'sm:-mx-2!', + name: 'mx-2', + prefix: '-mx', + important: true, + negated: true, + loc: { start: 9, end: 11 }, + spans: [ + { start: 0, end: 3 }, + { start: 6, end: 11 }, + ], + variants: [ + { + raw: 'sm:', + name: 'sm', + loc: { start: 6, end: 9 }, + }, + ], + }, + { + raw: '8', + value: 'xl:-mx-8', + name: 'mx-8', + prefix: '-mx', + important: false, + negated: true, + loc: { start: 15, end: 16 }, + spans: [ + { start: 0, end: 3 }, + { start: 12, end: 16 }, + ], + variants: [ + { + raw: 'xl:', + name: 'xl', + loc: { start: 12, end: 15 }, + }, + ], + }, + ], + ], + [ + 'ring(focus:& offset(& width)', + [ + { + raw: '&', + value: 'focus:ring', + name: 'ring', + prefix: 'ring', + important: false, + negated: false, + loc: { start: 11, end: 12 }, + spans: [ + { start: 0, end: 4 }, + { start: 5, end: 12 }, + ], + variants: [ + { + raw: 'focus:', + name: 'focus', + loc: { start: 5, end: 11 }, + }, + ], + }, + { + raw: '&', + value: 'ring-offset', + name: 'ring-offset', + prefix: 'ring-offset', + important: false, + negated: false, + loc: { start: 20, end: 21 }, + spans: [ + { start: 0, end: 4 }, + { start: 13, end: 19 }, + { start: 20, end: 21 }, + ], + variants: [], + }, + { + raw: 'width', + value: 'ring-offset-width', + name: 'ring-offset-width', + prefix: 'ring-offset', + important: false, + negated: false, + loc: { start: 22, end: 27 }, + spans: [ + { start: 0, end: 4 }, + { start: 13, end: 19 }, + { start: 22, end: 27 }, + ], + variants: [], + }, + ], + ], + [ + 'text(xl hover:underline) font-bold', + [ + { + raw: 'xl', + value: 'text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 5, end: 7 }, + spans: [ + { start: 0, end: 4 }, + { start: 5, end: 7 }, + ], + variants: [], + }, + { + raw: 'underline', + value: 'hover:text-underline', + name: 'text-underline', + prefix: 'text', + important: false, + negated: false, + loc: { start: 14, end: 23 }, + spans: [ + { start: 0, end: 4 }, + { start: 8, end: 23 }, + ], + variants: [ + { + raw: 'hover:', + name: 'hover', + loc: { start: 8, end: 14 }, + }, + ], + }, + { + raw: 'font-bold', + value: 'font-bold', + name: 'font-bold', + prefix: '', + important: false, + negated: false, + loc: { start: 25, end: 34 }, + spans: [{ start: 25, end: 34 }], + variants: [], + }, + ], + ], +] as const).forEach(([input, expected]) => { + test(`parse: ${JSON.stringify(input)}`, () => { + // console.log(JSON.stringify(parse(input))) + assert.equal(parse(input), expected) + }) +}) + +/** + * Info at position use case + */ +;([ + [ + 'underline', + 0, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'underline', + 1, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'underline', + 9, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 0, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 8, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 9, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + { + raw: 'font-bold', + value: 'font-bold', + name: 'font-bold', + prefix: '', + important: false, + negated: false, + loc: { start: 25, end: 34 }, + spans: [{ start: 25, end: 34 }], + variants: [], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 10, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 13, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 14, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 15, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 17, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 18, + [ + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 20, + [ + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 21, + [ + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 23, + [ + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + ], + ], + [ + 'underline text(lg md:xl) font-bold', + 24, + [ + { + raw: 'underline', + value: 'underline', + name: 'underline', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 9 }, + spans: [{ start: 0, end: 9 }], + variants: [], + }, + { + raw: 'lg', + value: 'text-lg', + name: 'text-lg', + prefix: 'text', + important: false, + negated: false, + loc: { start: 15, end: 17 }, + spans: [ + { start: 10, end: 14 }, + { start: 15, end: 17 }, + ], + variants: [], + }, + { + raw: 'xl', + value: 'md:text-xl', + name: 'text-xl', + prefix: 'text', + important: false, + negated: false, + loc: { start: 21, end: 23 }, + spans: [ + { start: 10, end: 14 }, + { start: 18, end: 23 }, + ], + variants: [ + { + raw: 'md:', + name: 'md', + loc: { start: 18, end: 21 }, + }, + ], + }, + { + raw: 'font-bold', + value: 'font-bold', + name: 'font-bold', + prefix: '', + important: false, + negated: false, + loc: { start: 25, end: 34 }, + spans: [{ start: 25, end: 34 }], + variants: [], + }, + ], + ], +] as const).forEach(([input, position, expected]) => { + test(`parse (position=${position}): ${JSON.stringify(input)}`, () => { + assert.equal(parse(input, position), expected) + }) +}) + +/** + * Completion at position use case + */ +;([ + [ + 'underline', + 0, + { + raw: '', + value: '', + name: '', + prefix: '', + important: false, + negated: false, + loc: { start: 0, end: 0 }, + spans: [{ start: 0, end: 0 }], + variants: [], + }, + ], +] as const).forEach(([input, position, expected]) => { + test(`parse (position=${position}, exact): ${JSON.stringify(input)}`, () => { + assert.equal(parse(input, position, true), expected) + }) +}) + +test.run() diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..8e3e781 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,423 @@ +export interface Rule { + raw: string + + value: string + + name: string + prefix: string + + /** + * Something like `-mx` + */ + negated: boolean + + /** + * Something like `underline!` or `bg-red-500!` or `red-500!` + */ + important: boolean + + variants: RuleVariant[] + + loc: TextRange + spans: TextRange[] +} + +export interface RuleVariant { + /** + * Raw value like: `hover:` or `after::` + */ + raw: string + + /** + * Name without last colon like: `hover` or `after:` + */ + name: string + + loc: TextRange +} + +export function parse(input: string, position?: number): Rule[] +export function parse(input: string, position: number, exact: boolean): Rule + +export function parse(input: string, position?: number, exact?: boolean): Rule[] | Rule { + const root = astish(input, exact ? position : undefined) + + if (exact) { + let node: Exclude = root + + while (node.next) { + node = node.next + } + + return toRule( + node.kind === NodeKind.Identifier ? node : createIdentifier(node, node, '', node.loc.end), + ) + } + + const rules: Rule[] = [] + + for (let node: Node | null = root; (node = node.next); ) { + if (node.kind === NodeKind.Identifier && node.terminator) { + rules.push(toRule(node)) + } + } + + if (position != null) { + const rulesAtPosition = rules.filter((rule) => + rule.spans.some((loc) => loc.start <= position && position < loc.end), + ) + + if (rulesAtPosition.length) { + return rulesAtPosition + } + + // Find closest group for position and check rules + let group = root.loc + + for (let node: Node = root; node; node = node.next) { + if ( + node.kind === NodeKind.Group && + node.loc.start <= position && + position < node.loc.end && + (group.start < node.loc.start || node.loc.start < group.end) + ) { + group = node.loc + } + } + + return rules.filter((rule) => + rule.spans.some((loc) => group.start <= loc.start && loc.end <= group.end), + ) + } + return rules +} + +function toRuleVariant(node: Variant): RuleVariant { + return { raw: node.raw, name: node.name, loc: node.loc } +} + +function toRule(identifier: Identifier): Rule { + const rule: Rule = { + raw: identifier.raw, + value: '', + name: '', + prefix: '', + important: false, + negated: false, + loc: identifier.loc, + spans: [], + variants: [], + } + + const names = [] + + for (let node: Node | null = identifier; node; node = node.parent) { + // join consecutive spans + const loc = { ...node.loc } + if (node.kind !== NodeKind.Group) { + const firstSpan = rule.spans[0] + if (firstSpan?.start === loc.end) { + firstSpan.start = loc.start + } else { + rule.spans.unshift(loc) + } + } + + if (node.kind === NodeKind.Identifier) { + rule.important = rule.important || node.important + rule.negated = rule.negated || node.negated + names.unshift(node.name) + } else if (node.kind === NodeKind.Variant) { + rule.variants.unshift(toRuleVariant(node)) + } + } + + rule.name = names.reduce( + (name, part) => (part === '&' ? name : name && part ? `${name}-${part}` : name || part), + '', + ) + + rule.prefix = names + .slice(0, -1) + .reduce( + (name, part) => (part === '&' ? name : name && part ? `${name}-${part}` : name || part), + '', + ) + + if (rule.prefix && rule.negated) { + rule.prefix = '-' + rule.prefix + } + + rule.value = + rule.variants.map((variant) => variant.raw).join('') + + (rule.negated ? '-' : '') + + rule.name + + (rule.important ? '!' : '') + + return rule +} + +export interface TextRange { + start: number + end: number +} + +export const enum NodeKind { + Group = 1, + Identifier = 2, + Variant = 3, +} + +export type Node = Variant | Identifier | Group | null + +/** + * `text(lg sm:base) hover:underline` + * + * text -> identifier: prev: null, next: group, parent: root + * group -> group: prev: text, next: lg, parent: text + * lg -> identifier: prev: text, next: sm, parent: group, end: true + * whitespace -> parent = node.parent.kind = 'Group' -> parent = group + * sm -> variant: prev: lg, next: base, parent: group + * base -> identifier: prev: sm, next: hover, parent: sm, end: true + * ) -> whitespace -> parent = group -> parent.parent.kind = 'Group' -> parent = root + * hover -> variant: prev: base, next: underline, parent: root + * underline -> variant: prev: hover, next: null, parent: hover, end: true + */ +export interface BaseNode { + /** Points to previous node */ + prev: Node + + /** Points to next node */ + next: Node + + /** Points to parent node */ + parent: Node + + loc: TextRange +} + +export interface Identifier extends BaseNode { + kind: NodeKind.Identifier + raw: string + name: string + + terminator: boolean + + /** + * Something like `-mx` + */ + negated: boolean + + /** + * Something like `underline!` or `bg-red-500!` or `red-500!` + */ + important: boolean +} + +/** + * Something like `hover:` or `after::` + */ +export interface Variant extends BaseNode { + kind: NodeKind.Variant + + /** + * Raw value like: `hover:` or `after::` + */ + raw: string + + /** + * Name without last colon like: `hover` or `after:` + */ + name: string +} + +/** + * `(text-bold)` or `(bold underline)` + */ +export interface Group extends BaseNode { + kind: NodeKind.Group + + // body: (Variant | Identifier | Group)[] +} + +export function astish(text: string, atPosition = Infinity): Group { + let buffer = '' + let start = 0 + + const root: Group = { + kind: NodeKind.Group, + + prev: null, + next: null, + parent: null, + + loc: { start, end: text.length }, + } + + let parent: Exclude = root + let node: Exclude = root + + for (let char: string, position = 0; position < text.length; position++) { + if (position >= atPosition) { + node.next = createIdentifier(node, parent, buffer, start) + + return root + } + + switch ((char = text[position])) { + case ':': + if (buffer) { + buffer += char + + if (text[position + 1] == ':') { + buffer += text[position++] + } + + parent = node = node.next = createVariant(node, parent, buffer, start) + + buffer = '' + start = position + 1 + } else { + // Invalid + } + + break + + case '(': { + // If there is a token this is the prefix for all grouped tokens + if (buffer) { + parent = node = node.next = createIdentifier(node, parent, buffer, start) + } + + parent = node = node.next = createGroup(node, parent, position) + + buffer = '' + start = position + 1 + + break + } + + case ')': + case ' ': + case '\t': + case '\n': + case '\r': + if (buffer) { + parent = node = node.next = createIdentifier( + node, + parent, + buffer, + start, + /* terminator */ true, + ) + } + + parent = getParentGroup(parent) || root + + if (char == ')') { + parent.loc.end = position + 1 + parent = parent.parent || root + } + + buffer = '' + start = position + 1 + + break + + default: + buffer += char + } + } + + // Consume remaining buffer + if (buffer) { + node.next = createIdentifier(node, parent, buffer, start, true) + } + + return root +} + +function getParentGroup(node: Node): Group | null { + while (node && node.kind != NodeKind.Group) { + node = node.parent + } + + return node +} + +function createGroup(node: Node, parent: Node, position: number): Group { + return { + kind: NodeKind.Group, + prev: node, + next: null, + parent, + loc: { + start: position, + end: position + 1, + }, + } +} + +function createVariant(node: Node, parent: Node, raw: string, start: number): Variant { + return { + kind: NodeKind.Variant, + + prev: node, + next: null, + parent, + + /** + * Raw value like: `hover:` or `after::` + */ + raw, + + /** + * Name without last colon like: `hover` or `after:` + */ + name: raw.slice(0, -1), + + // isPseudoElement: raw.endsWith('::'), + + loc: { + start, + end: start + raw.length, + }, + } +} + +function createIdentifier( + node: Node, + parent: Node, + raw: string, + start: number, + terminator = false, +): Identifier { + let name = raw + let negated = false + let important = false + if (name[0] == '-') { + name = name.slice(1) + negated = true + } + + if (name[name.length - 1] == '!') { + name = name.slice(0, -1) + important = true + } + + return { + kind: NodeKind.Identifier, + prev: node, + next: null, + parent, + raw, + name, + terminator, + negated, + important, + loc: { + start, + end: start + raw.length, + }, + } +} diff --git a/src/twind.ts b/src/twind.ts index c7c6a27..c533122 100644 --- a/src/twind.ts +++ b/src/twind.ts @@ -187,7 +187,8 @@ export interface CompletionToken { export interface Completions { tokens: CompletionToken[] - screens: string[] + screens: Set + variants: Set } export class Twind { @@ -297,7 +298,7 @@ export class Twind { const { state } = this if (!state) { - return { screens: [], tokens: [] } + return { screens: new Set(), variants: new Set(), tokens: [] } } const { program, config, sheet, tw, context } = state @@ -390,7 +391,8 @@ export class Twind { const INTERPOLATION_RE = /{{([^}]+)}}/ const completionTokens = new Map() - const screens = Object.keys(theme('screens')(context)).map((x) => x + ':') + const screens = new Set(Object.keys(theme('screens')(context)).map((x) => x + ':')) + tokens.unshift(...screens) tokens.forEach((directive): void => { const match = INTERPOLATION_RE.exec(directive) @@ -444,7 +446,7 @@ export class Twind { completionTokens.set( className, createCompletionToken(className, { - kind: screens.includes(className) ? 'screen' : undefined, + kind: screens.has(className) ? 'screen' : undefined, raw: directive, theme: { section: sectionKey as keyof Theme, key, value: section[key] }, }), @@ -474,9 +476,18 @@ export class Twind { } }) + const variants = new Set() + + for (const completionToken of completionTokens.values()) { + if (completionToken.kind !== 'utility') { + variants.add(completionToken.value) + } + } + return { tokens: [...completionTokens.values()], screens, + variants, } } } diff --git a/test-env.js b/test-env.js new file mode 100644 index 0000000..7255709 --- /dev/null +++ b/test-env.js @@ -0,0 +1,8 @@ +/* eslint-env node */ +require('esbuild-register') + +// Node's error-message stack size is limited at 10, but it's pretty useful +// to see more than that when a test fails. +Error.stackTraceLimit = 100 + +process.env.NODE_ENV = 'test' diff --git a/yarn.lock b/yarn.lock index 9a9a147..768fddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,6 +327,16 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +dequal@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" + integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== + +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -408,12 +418,20 @@ es-module-lexer@^0.3.26: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz#7b507044e97d5b03b01d4392c74ffeb9c177a83b" integrity sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA== +esbuild-register@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-2.3.0.tgz#c7c6e2cabdce0e7aa5f30be774302d3fc1ebd02e" + integrity sha512-uT3WXEQGAqzrI0SLy1Jz39BzIBiLWd5La9zFZ+FUSCPGqJbE+ZJHUTE8yHP1GVfyHKrrAFCZqLieaHkSprIRDQ== + dependencies: + esbuild "^0.9.2" + jsonc-parser "^3.0.0" + esbuild@^0.8.28: version "0.8.57" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.57.tgz#a42d02bc2b57c70bcd0ef897fe244766bb6dd926" integrity sha512-j02SFrUwFTRUqiY0Kjplwjm1psuzO1d6AjaXKuOR9hrY0HuPsT6sV42B6myW34h1q4CRy+Y3g4RU/cGJeI/nNA== -esbuild@^0.9.1: +esbuild@^0.9.1, esbuild@^0.9.2: version "0.9.2" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.9.2.tgz#7e9fde247c913ed8ee059e2648b0c53f7d00abe5" integrity sha512-xE3oOILjnmN8PSjkG3lT9NBbd1DbxNqolJ5qNyrLhDWsFef3yTp/KTQz1C/x7BYFKbtrr9foYtKA6KA1zuNAUQ== @@ -503,6 +521,11 @@ eslint@^7.15.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^7.3.0, espree@^7.3.1: version "7.3.1" resolved "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" @@ -816,6 +839,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + +kleur@^4.0.3: + version "4.1.4" + resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" + integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -888,6 +921,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +mri@^1.1.0, mri@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6" + integrity sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1076,6 +1114,13 @@ run-parallel@^1.1.9: resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz#60a51b2ae836636c81377df16cb107351bcd13ef" integrity sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw== +sade@^1.7.3: + version "1.7.4" + resolved "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz#ea681e0c65d248d2095c90578c03ca0bb1b54691" + integrity sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA== + dependencies: + mri "^1.1.0" + semver@^7.2.1, semver@^7.3.2: version "7.3.4" resolved "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" @@ -1198,6 +1243,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +totalist@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/totalist/-/totalist-2.0.0.tgz#db6f1e19c0fa63e71339bbb8fba89653c18c7eec" + integrity sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ== + tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -1248,6 +1298,17 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +uvu@^0.5.1: + version "0.5.1" + resolved "https://registry.npmjs.org/uvu/-/uvu-0.5.1.tgz#938b85f96b8a478e585363ad1849933b6c481b28" + integrity sha512-JGxttnOGDFs77FaZ0yMUHIzczzQ5R1IlDeNW6Wymw6gAscwMdAffVOP6TlxLIfReZyK8tahoGwWZaTCJzNFDkg== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + totalist "^2.0.0" + v8-compile-cache@^2.0.3: version "2.2.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" @@ -1258,6 +1319,13 @@ vscode-languageserver-types@^3.13.0: resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== +watchlist@^0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/watchlist/-/watchlist-0.2.3.tgz#90af76d7d0d4c00b8b0eecddae1c247447f86136" + integrity sha512-xStuPg489QXZbRirnmIMo7OaKFnGkvTQn7tCUC/sVmVVEvDQQnnVl/k9D5yg3nXgpebgPHpfApBLHMpEbAqvSQ== + dependencies: + mri "^1.1.5" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"